Initial commit
This commit is contained in:
parent
8cc8a855eb
commit
0f85b31208
23
.gitignore
vendored
Normal file
23
.gitignore
vendored
Normal file
@ -0,0 +1,23 @@
|
||||
# Generated by Cargo
|
||||
# will have compiled files and executables
|
||||
debug/
|
||||
target/
|
||||
|
||||
# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
|
||||
# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
|
||||
Cargo.lock
|
||||
|
||||
# These are backup files generated by rustfmt
|
||||
**/*.rs.bk
|
||||
|
||||
# MSVC Windows builds of rustc generate these, which store debugging information
|
||||
*.pdb
|
||||
|
||||
# node.js
|
||||
node_modules/
|
||||
|
||||
# wrangler
|
||||
.wrangler/
|
||||
|
||||
# VSCode
|
||||
.vscode/
|
22
Cargo.toml
Normal file
22
Cargo.toml
Normal file
@ -0,0 +1,22 @@
|
||||
[package]
|
||||
name = "worker-rust"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
# https://github.com/rustwasm/wasm-pack/issues/1247
|
||||
[package.metadata.wasm-pack.profile.release]
|
||||
wasm-opt = false
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib"]
|
||||
|
||||
[dependencies]
|
||||
async-trait = "0.1.74"
|
||||
base64 = "0.21.5"
|
||||
lazy_static = "1.4.0"
|
||||
worker = "0.0.15"
|
||||
|
||||
[profile.release]
|
||||
lto = true
|
||||
strip = true
|
||||
codegen-units = 1
|
51
README.md
Normal file
51
README.md
Normal file
@ -0,0 +1,51 @@
|
||||
# Template: worker-rust
|
||||
|
||||
[](https://deploy.workers.cloudflare.com/?url=https://github.com/cloudflare/templates/tree/main/worker-rust)
|
||||
|
||||
A template for kick starting a Cloudflare worker project using [`workers-rs`](https://github.com/cloudflare/workers-rs).
|
||||
|
||||
This template is designed for compiling Rust to WebAssembly and publishing the resulting worker to Cloudflare's [edge infrastructure](https://www.cloudflare.com/network/).
|
||||
|
||||
## Setup
|
||||
|
||||
To create a `my-project` directory using this template, run:
|
||||
|
||||
```sh
|
||||
$ npx wrangler generate my-project https://github.com/cloudflare/workers-sdk/templates/experimental/worker-rust
|
||||
# or
|
||||
$ yarn wrangler generate my-project https://github.com/cloudflare/workers-sdk/templates/experimental/worker-rust
|
||||
# or
|
||||
$ pnpm wrangler generate my-project https://github.com/cloudflare/workers-sdk/templates/experimental/worker-rust
|
||||
```
|
||||
|
||||
## Wrangler
|
||||
|
||||
Wrangler is used to develop, deploy, and configure your Worker via CLI.
|
||||
|
||||
Further documentation for Wrangler can be found [here](https://developers.cloudflare.com/workers/tooling/wrangler).
|
||||
|
||||
## Usage
|
||||
|
||||
This template starts you off with a `src/lib.rs` file, acting as an entrypoint for requests hitting your Worker. Feel free to add more code in this file, or create Rust modules anywhere else for this project to use.
|
||||
|
||||
With `wrangler`, you can build, test, and deploy your Worker with the following commands:
|
||||
|
||||
```sh
|
||||
# run your Worker in an ideal development workflow (with a local server, file watcher & more)
|
||||
$ npm run dev
|
||||
|
||||
# deploy your Worker globally to the Cloudflare network (update your wrangler.toml file for configuration)
|
||||
$ npm run deploy
|
||||
```
|
||||
|
||||
Read the latest `worker` crate documentation here: https://docs.rs/worker
|
||||
|
||||
## WebAssembly
|
||||
|
||||
`workers-rs` (the Rust SDK for Cloudflare Workers used in this template) is meant to be executed as compiled WebAssembly, and as such so **must** all the code you write and depend upon. All crates and modules used in Rust-based Workers projects have to compile to the `wasm32-unknown-unknown` triple.
|
||||
|
||||
Read more about this on the [`workers-rs`](https://github.com/cloudflare/workers-rs) project README.
|
||||
|
||||
## Issues
|
||||
|
||||
If you have any problems with the `worker` crate, please open an issue on the upstream project issue tracker on the [`workers-rs` repository](https://github.com/cloudflare/workers-rs).
|
12
package.json
Normal file
12
package.json
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"name": "r2-webdav",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"deploy": "wrangler deploy",
|
||||
"dev": "wrangler dev --local"
|
||||
},
|
||||
"devDependencies": {
|
||||
"wrangler": "^2.13.0"
|
||||
}
|
||||
}
|
346
src/dav.rs
Normal file
346
src/dav.rs
Normal file
@ -0,0 +1,346 @@
|
||||
use worker::ByteStream;
|
||||
|
||||
use crate::r2::R2;
|
||||
use crate::values::{Depth, Overwrite, Range};
|
||||
use crate::xml::XMLBuilder;
|
||||
use std::collections::HashMap;
|
||||
use std::option::Option;
|
||||
|
||||
pub struct Dav {
|
||||
fs: R2,
|
||||
}
|
||||
|
||||
type DavResponse = (u16, HashMap<String, String>, String);
|
||||
type DavErrResponse = (u16, Option<HashMap<String, String>>, Option<String>);
|
||||
type DavStreamResponse = (u16, HashMap<String, String>, ByteStream);
|
||||
|
||||
pub enum DavResponseType {
|
||||
DavResponse(Result<DavResponse, DavErrResponse>),
|
||||
DavStreamResponse(Result<DavStreamResponse, DavErrResponse>),
|
||||
}
|
||||
|
||||
impl From<Result<DavResponse, DavErrResponse>> for DavResponseType {
|
||||
fn from(value: Result<DavResponse, DavErrResponse>) -> Self {
|
||||
DavResponseType::DavResponse(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Result<DavStreamResponse, DavErrResponse>> for DavResponseType {
|
||||
fn from(value: Result<DavStreamResponse, DavErrResponse>) -> Self {
|
||||
DavResponseType::DavStreamResponse(value)
|
||||
}
|
||||
}
|
||||
|
||||
static DAV_CLASS: &str = "1";
|
||||
static SUPPORT_METHODS: [&str; 8] = [
|
||||
"OPTIONS", "PROPFIND", "MKCOL", "GET", "HEAD", "PUT", "COPY", "MOVE",
|
||||
];
|
||||
|
||||
impl Dav {
|
||||
pub fn new(fs: R2) -> Dav {
|
||||
Dav { fs }
|
||||
}
|
||||
|
||||
pub async fn handle_unsupport_method(&self) -> Result<DavResponse, DavErrResponse> {
|
||||
let mut headers = HashMap::new();
|
||||
headers.insert("DAV".to_string(), DAV_CLASS.to_string());
|
||||
headers.insert("Allow".to_string(), SUPPORT_METHODS.join(", ").to_string());
|
||||
return Err((405, Some(headers), None));
|
||||
}
|
||||
|
||||
pub async fn handle_options(&self) -> Result<DavResponse, DavErrResponse> {
|
||||
let mut headers = HashMap::new();
|
||||
headers.insert("DAV".to_string(), DAV_CLASS.to_string());
|
||||
headers.insert("Allow".to_string(), SUPPORT_METHODS.join(", ").to_string());
|
||||
return Ok((204, headers, "".to_string()));
|
||||
}
|
||||
|
||||
pub async fn handle_propfind(
|
||||
&self,
|
||||
path: String,
|
||||
depth: Depth,
|
||||
req_body: String,
|
||||
) -> Result<DavResponse, DavErrResponse> {
|
||||
if req_body.len() > 0 {
|
||||
return Err((415, None, None));
|
||||
}
|
||||
|
||||
let mut headers = HashMap::new();
|
||||
headers.insert(
|
||||
"Content-Type".to_string(),
|
||||
"application/xml; charset=utf-8".to_string(),
|
||||
);
|
||||
|
||||
match depth {
|
||||
Depth::One => {
|
||||
let mut multistatus = XMLBuilder::new(
|
||||
"D:multistatus".to_string(),
|
||||
Some(vec![("xmlns:D".to_string(), "DAV:".to_string())]),
|
||||
None,
|
||||
);
|
||||
match self.fs.list(path).await {
|
||||
Ok(items) => {
|
||||
for (href, properties) in items {
|
||||
let mut response =
|
||||
XMLBuilder::new("D:response".to_string(), None, None);
|
||||
response.elem("D:href".to_string(), None, Some(href));
|
||||
let mut propstat =
|
||||
XMLBuilder::new("D:propstat".to_string(), None, None);
|
||||
let mut prop = XMLBuilder::new("D:prop".to_string(), None, None);
|
||||
prop.elem("D:creationdate".to_string(), None, properties.creation_date);
|
||||
prop.elem("D:displayname".to_string(), None, properties.display_name);
|
||||
prop.elem(
|
||||
"D:getcontentlanguage".to_string(),
|
||||
None,
|
||||
properties.get_content_language,
|
||||
);
|
||||
prop.elem(
|
||||
"D:getcontentlength".to_string(),
|
||||
None,
|
||||
properties
|
||||
.get_content_length
|
||||
.map_or(None, |v| Some(v.to_string())),
|
||||
);
|
||||
prop.elem(
|
||||
"D:getcontenttype".to_string(),
|
||||
None,
|
||||
properties.get_content_type,
|
||||
);
|
||||
prop.elem("D:getetag".to_string(), None, properties.get_etag);
|
||||
prop.elem(
|
||||
"D:getlastmodified".to_string(),
|
||||
None,
|
||||
properties.get_last_modified,
|
||||
);
|
||||
propstat.add(prop);
|
||||
propstat.elem(
|
||||
"D:status".to_string(),
|
||||
None,
|
||||
Some("HTTP/1.1 200 OK".to_string()),
|
||||
);
|
||||
response.add(propstat);
|
||||
multistatus.add(response);
|
||||
}
|
||||
|
||||
Ok((207, headers, multistatus.build()))
|
||||
}
|
||||
Err(_) => return Err((404, None, None)),
|
||||
}
|
||||
}
|
||||
Depth::Zero => {
|
||||
let mut multistatus = XMLBuilder::new(
|
||||
"D:multistatus".to_string(),
|
||||
Some(vec![("xmlns:D".to_string(), "DAV:".to_string())]),
|
||||
None,
|
||||
);
|
||||
match self.fs.get(path).await {
|
||||
Ok((href, properties)) => {
|
||||
let mut response = XMLBuilder::new("D:response".to_string(), None, None);
|
||||
response.elem("D:href".to_string(), None, Some(href));
|
||||
let mut propstat = XMLBuilder::new("D:propstat".to_string(), None, None);
|
||||
let mut prop = XMLBuilder::new("D:prop".to_string(), None, None);
|
||||
prop.elem("D:creationdate".to_string(), None, properties.creation_date);
|
||||
prop.elem("D:displayname".to_string(), None, properties.display_name);
|
||||
prop.elem(
|
||||
"D:getcontentlanguage".to_string(),
|
||||
None,
|
||||
properties.get_content_language,
|
||||
);
|
||||
prop.elem(
|
||||
"D:getcontentlength".to_string(),
|
||||
None,
|
||||
properties
|
||||
.get_content_length
|
||||
.map_or(None, |v| Some(v.to_string())),
|
||||
);
|
||||
prop.elem(
|
||||
"D:getcontenttype".to_string(),
|
||||
None,
|
||||
properties.get_content_type,
|
||||
);
|
||||
prop.elem("D:getetag".to_string(), None, properties.get_etag);
|
||||
prop.elem(
|
||||
"D:getlastmodified".to_string(),
|
||||
None,
|
||||
properties.get_last_modified,
|
||||
);
|
||||
propstat.add(prop);
|
||||
propstat.elem(
|
||||
"D:status".to_string(),
|
||||
None,
|
||||
Some("HTTP/1.1 200 OK".to_string()),
|
||||
);
|
||||
response.add(propstat);
|
||||
multistatus.add(response);
|
||||
|
||||
Ok((207, (headers), (multistatus.build())))
|
||||
}
|
||||
Err(_) => return Err((404, None, None)),
|
||||
}
|
||||
}
|
||||
Depth::Infinity => return Err((400, None, None)),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn handle_mkcol(
|
||||
&self,
|
||||
path: String,
|
||||
req_body: String,
|
||||
) -> Result<DavResponse, DavErrResponse> {
|
||||
if req_body.len() > 0 {
|
||||
return Err((415, None, None));
|
||||
}
|
||||
Ok((201, HashMap::new(), "".to_string()))
|
||||
// R2 unsupport create empty directory
|
||||
// Err((403, None, None))
|
||||
}
|
||||
|
||||
pub async fn handle_get_obj(
|
||||
&self,
|
||||
path: String,
|
||||
range: Range,
|
||||
) -> Result<DavStreamResponse, DavErrResponse> {
|
||||
match self.fs.download(path, range.clone()).await {
|
||||
Ok((properties, stream)) => {
|
||||
let mut headers: HashMap<String, String> = HashMap::new();
|
||||
headers.insert("Accept-Ranges".to_string(), "bytes".to_string());
|
||||
headers.insert(
|
||||
"Content-Type".to_string(),
|
||||
properties
|
||||
.get_content_type
|
||||
.map_or("application/octet-stream".to_string(), |v| v),
|
||||
);
|
||||
headers.insert(
|
||||
"Content-Length".to_string(),
|
||||
properties
|
||||
.get_content_length
|
||||
.map_or("0".to_string(), |v| v.to_string()),
|
||||
);
|
||||
properties
|
||||
.get_etag
|
||||
.map(|v| headers.insert("ETag".to_string(), v));
|
||||
properties
|
||||
.get_last_modified
|
||||
.map(|v| headers.insert("Last-Modified".to_string(), v));
|
||||
match (range.start, range.end) {
|
||||
(Some(start), Some(end)) => {
|
||||
headers.insert(
|
||||
"Content-Range".to_string(),
|
||||
format!("bytes {}-{}/{}", start, end, end - start + 1),
|
||||
);
|
||||
Ok((206, (headers), stream))
|
||||
}
|
||||
_ => Ok((200, (headers), stream)),
|
||||
}
|
||||
}
|
||||
Err(_) => return Err((404, None, None)),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn handle_get_dir(&self, path: String) -> Result<DavResponse, DavErrResponse> {
|
||||
match self.fs.list(path).await {
|
||||
Ok(items) => {
|
||||
let mut headers = HashMap::new();
|
||||
headers.insert(
|
||||
"Content-Type".to_string(),
|
||||
"application/html; charset=utf-8".to_string(),
|
||||
);
|
||||
let html = items
|
||||
.iter()
|
||||
.map(|item| {
|
||||
format!(
|
||||
"<a href=\"{}\">{}</a>",
|
||||
&item.0,
|
||||
match &item.1.display_name {
|
||||
Some(display_name) => display_name,
|
||||
None => &item.0,
|
||||
}
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ");
|
||||
return Ok((200, (headers), (html)));
|
||||
}
|
||||
Err(_) => return Err((404, None, None)),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn handle_head_obj(
|
||||
&self,
|
||||
path: String,
|
||||
range: Range,
|
||||
) -> Result<DavResponse, DavErrResponse> {
|
||||
match self.handle_get_obj(path, range).await {
|
||||
Ok((status_code, headers, _)) => Ok((status_code, headers, "".to_string())),
|
||||
Err(e) => Err(e),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn handle_head_dir(&self, path: String) -> Result<DavResponse, DavErrResponse> {
|
||||
match self.handle_get_dir(path).await {
|
||||
Ok((status_code, headers, _)) => Ok((status_code, headers, "".to_string())),
|
||||
Err(e) => Err(e),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn handle_delete(&self, path: String) -> Result<DavResponse, DavErrResponse> {
|
||||
match self.fs.delete(path).await {
|
||||
Ok(()) => Ok((204, HashMap::new(), "".to_string())),
|
||||
Err(error) => Err((400, None, Some(error.to_string()))),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn handle_put(
|
||||
&self,
|
||||
path: String,
|
||||
stream: ByteStream,
|
||||
content_length: u64,
|
||||
) -> Result<DavResponse, DavErrResponse> {
|
||||
if path.ends_with("/") {
|
||||
return Err((405, None, None));
|
||||
}
|
||||
match self.fs.put(path, stream, content_length).await {
|
||||
Ok(properties) => {
|
||||
println!("{:?}", properties);
|
||||
Ok((201, HashMap::new(), "".to_string()))
|
||||
}
|
||||
Err(error) => Err((400, None, Some(error.to_string()))),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn handle_copy(
|
||||
&self,
|
||||
path: String,
|
||||
destination: String,
|
||||
depth: Depth,
|
||||
overwrite: Overwrite,
|
||||
) -> Result<DavResponse, DavErrResponse> {
|
||||
if path.ends_with("/") {
|
||||
match depth {
|
||||
Depth::Zero => Err((400, None, Some("Unsupported copy collection".to_string()))),
|
||||
Depth::Infinity => Ok((200, HashMap::new(), "".to_string())),
|
||||
_ => Err((400, None, Some("Unsupported copy depth".to_string()))),
|
||||
}
|
||||
} else {
|
||||
Err((400, None, Some("Unsupported copy resource".to_string())))
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn handle_move(
|
||||
&self,
|
||||
path: String,
|
||||
destination: String,
|
||||
depth: Depth,
|
||||
overwrite: Overwrite,
|
||||
) -> Result<DavResponse, DavErrResponse> {
|
||||
if path.ends_with("/") {
|
||||
match depth {
|
||||
Depth::Zero => Err((400, None, Some("Unsupported move collection".to_string()))),
|
||||
Depth::Infinity => Ok((200, HashMap::new(), "".to_string())),
|
||||
_ => Err((400, None, Some("Unsupported move depth".to_string()))),
|
||||
}
|
||||
} else {
|
||||
Err((400, None, Some("Unsupported move resource".to_string())))
|
||||
}
|
||||
}
|
||||
}
|
254
src/lib.rs
Normal file
254
src/lib.rs
Normal file
@ -0,0 +1,254 @@
|
||||
use crate::values::Depth;
|
||||
use base64;
|
||||
use dav::DavResponseType;
|
||||
use r2::R2;
|
||||
use values::Overwrite;
|
||||
use worker::*;
|
||||
|
||||
mod dav;
|
||||
mod r2;
|
||||
mod values;
|
||||
mod xml;
|
||||
|
||||
#[event(fetch)]
|
||||
async fn main(req: Request, env: Env, _: Context) -> Result<Response> {
|
||||
let username = env.var("USERNAME").unwrap().to_string();
|
||||
let password = env.var("PASSWORD").unwrap().to_string();
|
||||
let protocol = env.var("PROTOCOL").unwrap().to_string();
|
||||
let bucket_name = env.var("BUCKET_NAME").unwrap().to_string();
|
||||
|
||||
if let Some(r) = basic_authorization(
|
||||
req.headers().get("authorization").unwrap(),
|
||||
username,
|
||||
password,
|
||||
) {
|
||||
return r;
|
||||
}
|
||||
|
||||
let dav = dav::Dav::new(match protocol.as_str() {
|
||||
"r2" => R2::new(env.bucket(bucket_name.as_str()).unwrap()),
|
||||
_ => panic!("PROTOCOL {} not supported", protocol),
|
||||
});
|
||||
worker(req, dav).await
|
||||
}
|
||||
|
||||
fn basic_authorization(
|
||||
authorization_header: Option<String>,
|
||||
username: String,
|
||||
password: String,
|
||||
) -> Option<Result<Response>> {
|
||||
let basic_authorization_error_response = || {
|
||||
Some(Response::error("Unauthorized", 401).map(|response| {
|
||||
let mut headers = Headers::new();
|
||||
headers
|
||||
.append("WWW-Authenticate", "Basic realm=\"webdav\"")
|
||||
.unwrap();
|
||||
response.with_headers(headers)
|
||||
}))
|
||||
};
|
||||
|
||||
match authorization_header {
|
||||
Some(text) => {
|
||||
let a: Vec<&str> = text.split(" ").collect();
|
||||
if a.len() != 2 || a[0] != "Basic" {
|
||||
return basic_authorization_error_response();
|
||||
}
|
||||
if let Ok(v) = base64::decode(a[1]) {
|
||||
let v = match String::from_utf8(v) {
|
||||
Ok(v) => v,
|
||||
Err(_) => return basic_authorization_error_response(),
|
||||
};
|
||||
let v: Vec<&str> = v.split(":").collect();
|
||||
if v.len() != 2 {
|
||||
return basic_authorization_error_response();
|
||||
}
|
||||
if v[0] != username || v[1] != password {
|
||||
return basic_authorization_error_response();
|
||||
}
|
||||
|
||||
return None;
|
||||
} else {
|
||||
return basic_authorization_error_response();
|
||||
}
|
||||
}
|
||||
None => {
|
||||
return basic_authorization_error_response();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn worker(mut req: Request, dav: dav::Dav) -> Result<Response> {
|
||||
let dav_response: DavResponseType = match req.inner().method().as_str() {
|
||||
"PROPFIND" => {
|
||||
let depth: Depth = req
|
||||
.headers()
|
||||
.get("depth")
|
||||
.unwrap()
|
||||
.map_or("infinity".to_string(), |v| v)
|
||||
.into();
|
||||
let resource_path = req.path();
|
||||
dav.handle_propfind(resource_path, depth, req.text().await?)
|
||||
.await
|
||||
.into()
|
||||
}
|
||||
"OPTIONS" => dav.handle_options().await.into(),
|
||||
"MKCOL" => {
|
||||
let resource_path = req.path();
|
||||
dav.handle_mkcol(resource_path, req.text().await?)
|
||||
.await
|
||||
.into()
|
||||
}
|
||||
"GET" => {
|
||||
let resource_path = req.path();
|
||||
let range = req.headers().get("range").unwrap().map_or(
|
||||
values::Range {
|
||||
start: None,
|
||||
end: None,
|
||||
},
|
||||
|v| values::Range::from(v.to_string().split("bytes=").next().unwrap().to_string()),
|
||||
);
|
||||
if resource_path.ends_with("/") {
|
||||
dav.handle_get_dir(resource_path).await.into()
|
||||
} else {
|
||||
dav.handle_get_obj(resource_path, range).await.into()
|
||||
}
|
||||
}
|
||||
"HEAD" => {
|
||||
let resource_path = req.path();
|
||||
let range = req.headers().get("range").unwrap().map_or(
|
||||
values::Range {
|
||||
start: None,
|
||||
end: None,
|
||||
},
|
||||
|v| values::Range::from(v.to_string().split("bytes=").next().unwrap().to_string()),
|
||||
);
|
||||
if resource_path.ends_with("/") {
|
||||
dav.handle_head_dir(resource_path).await.into()
|
||||
} else {
|
||||
dav.handle_head_obj(resource_path, range).await.into()
|
||||
}
|
||||
}
|
||||
"DELETE" => {
|
||||
let resource_path = req.path();
|
||||
dav.handle_delete(resource_path).await.into()
|
||||
}
|
||||
"PUT" => {
|
||||
let resource_path = req.path();
|
||||
let content_length = req
|
||||
.headers()
|
||||
.get("content-length")
|
||||
.unwrap()
|
||||
.map_or(0, |v| v.parse::<u64>().unwrap());
|
||||
println!("content-length: {}", content_length);
|
||||
dav.handle_put(resource_path, req.stream().unwrap(), content_length)
|
||||
.await
|
||||
.into()
|
||||
}
|
||||
"COPY" => {
|
||||
let resource_path = req.path();
|
||||
let destination =
|
||||
req.headers()
|
||||
.get("destination")
|
||||
.unwrap()
|
||||
.map_or("".to_string(), |v| {
|
||||
v.split("http://")
|
||||
.nth(1)
|
||||
.unwrap()
|
||||
.split("/")
|
||||
.skip(1)
|
||||
.collect::<Vec<&str>>()
|
||||
.join("/")
|
||||
});
|
||||
let depth: Depth = req
|
||||
.headers()
|
||||
.get("depth")
|
||||
.unwrap()
|
||||
.map_or("infinity".to_string(), |v| v)
|
||||
.into();
|
||||
let overwrite: Overwrite = req
|
||||
.headers()
|
||||
.get("overwrite")
|
||||
.unwrap()
|
||||
.map_or("T".to_string(), |v| v.to_string())
|
||||
.into();
|
||||
dav.handle_copy(resource_path, destination, depth, overwrite)
|
||||
.await
|
||||
.into()
|
||||
}
|
||||
"MOVE" => {
|
||||
let resource_path = req.path();
|
||||
let destination =
|
||||
req.headers()
|
||||
.get("destination")
|
||||
.unwrap()
|
||||
.map_or("".to_string(), |v| {
|
||||
v.split("http://")
|
||||
.nth(1)
|
||||
.unwrap()
|
||||
.split("/")
|
||||
.skip(1)
|
||||
.collect::<Vec<&str>>()
|
||||
.join("/")
|
||||
});
|
||||
let depth: Depth = req
|
||||
.headers()
|
||||
.get("depth")
|
||||
.unwrap()
|
||||
.map_or("infinity".to_string(), |v| v)
|
||||
.into();
|
||||
let overwrite: Overwrite = req
|
||||
.headers()
|
||||
.get("overwrite")
|
||||
.unwrap()
|
||||
.map_or("T".to_string(), |v| v.to_string())
|
||||
.into();
|
||||
dav.handle_move(resource_path, destination, depth, overwrite)
|
||||
.await
|
||||
.into()
|
||||
}
|
||||
_ => dav.handle_unsupport_method().await.into(),
|
||||
};
|
||||
|
||||
match dav_response {
|
||||
DavResponseType::DavResponse(r) => r.map_or_else(
|
||||
|e| {
|
||||
let (status_code, headers, body) = e;
|
||||
Response::error(body.unwrap_or("".to_string()), status_code).map(|response| {
|
||||
match headers {
|
||||
Some(headers) => response.with_headers(Headers::from_iter(headers)),
|
||||
None => response,
|
||||
}
|
||||
.with_status(status_code)
|
||||
})
|
||||
},
|
||||
|r| {
|
||||
let (status_code, headers, body) = r;
|
||||
Response::from_body(ResponseBody::Body(body.into_bytes())).map(|response| {
|
||||
response
|
||||
.with_headers(Headers::from_iter(headers))
|
||||
.with_status(status_code)
|
||||
})
|
||||
},
|
||||
),
|
||||
DavResponseType::DavStreamResponse(r) => r.map_or_else(
|
||||
|e| {
|
||||
let (status_code, headers, body) = e;
|
||||
Response::error(body.unwrap_or("".to_string()), status_code).map(|response| {
|
||||
match headers {
|
||||
Some(headers) => response.with_headers(Headers::from_iter(headers)),
|
||||
None => response,
|
||||
}
|
||||
.with_status(status_code)
|
||||
})
|
||||
},
|
||||
|r| {
|
||||
let (status_code, headers, body) = r;
|
||||
Response::from_stream(body).map(|response| {
|
||||
response
|
||||
.with_headers(Headers::from_iter(headers))
|
||||
.with_status(status_code)
|
||||
})
|
||||
},
|
||||
),
|
||||
}
|
||||
}
|
100
src/r2.rs
Normal file
100
src/r2.rs
Normal file
@ -0,0 +1,100 @@
|
||||
use crate::values::{DavProperties, Range};
|
||||
use worker::{Bucket, ByteStream, FixedLengthStream, Range as R2Range};
|
||||
|
||||
pub struct R2 {
|
||||
bucket: Bucket,
|
||||
}
|
||||
|
||||
impl R2 {
|
||||
pub fn new(bucket: Bucket) -> R2 {
|
||||
R2 { bucket }
|
||||
}
|
||||
|
||||
pub async fn get(&self, path: String) -> Result<(String, DavProperties), String> {
|
||||
let result = self.bucket.get(path).execute().await;
|
||||
match result {
|
||||
Ok(f) => f.map_or(Err("Resource not found".to_string()), |file| {
|
||||
Ok((file.key(), DavProperties::from_r2(&file)))
|
||||
}),
|
||||
Err(error) => Err(error.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn list(&self, path: String) -> Result<Vec<(String, DavProperties)>, String> {
|
||||
match self.bucket.list().prefix(path).execute().await {
|
||||
Ok(files) => {
|
||||
let mut result = Vec::new();
|
||||
for file in files.objects() {
|
||||
result.push((file.key(), DavProperties::from_r2(&file)))
|
||||
}
|
||||
Ok(result)
|
||||
}
|
||||
Err(error) => Err(error.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn download(
|
||||
&self,
|
||||
path: String,
|
||||
range: Range,
|
||||
) -> Result<(DavProperties, ByteStream), String> {
|
||||
let r2range: Option<R2Range> = match (range.start, range.end) {
|
||||
(Some(start), Some(end)) => Some(R2Range::OffsetWithLength {
|
||||
offset: start,
|
||||
length: end - start + 1,
|
||||
}),
|
||||
(Some(start), None) => Some(R2Range::OffsetWithOptionalLength {
|
||||
offset: start,
|
||||
length: None,
|
||||
}),
|
||||
(None, Some(end)) => Some(R2Range::OptionalOffsetWithLength {
|
||||
offset: None,
|
||||
length: end,
|
||||
}),
|
||||
(None, None) => None,
|
||||
};
|
||||
let path_clone = path.clone();
|
||||
let result = r2range
|
||||
.map_or(self.bucket.get(path), |r| {
|
||||
self.bucket.get(path_clone).range(r)
|
||||
})
|
||||
.execute()
|
||||
.await;
|
||||
match result {
|
||||
Ok(f) => f.map_or(Err("Resource not found".to_string()), |file| {
|
||||
file.body()
|
||||
.map_or(Err("Failed to get file body stream".to_string()), |b| {
|
||||
b.stream().map_or(
|
||||
Err("Failed to get file body stream".to_string()),
|
||||
|stream| Ok((DavProperties::from_r2(&file), stream)),
|
||||
)
|
||||
})
|
||||
}),
|
||||
Err(error) => Err(error.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn delete(&self, path: String) -> Result<(), String> {
|
||||
match self.bucket.delete(path).await {
|
||||
Ok(_) => Ok(()),
|
||||
Err(error) => Err(error.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn put(
|
||||
&self,
|
||||
path: String,
|
||||
stream: ByteStream,
|
||||
content_length: u64,
|
||||
) -> Result<DavProperties, String> {
|
||||
match self
|
||||
.bucket
|
||||
.put(path, FixedLengthStream::wrap(stream, content_length))
|
||||
.execute()
|
||||
.await
|
||||
{
|
||||
Ok(file) => Ok(DavProperties::from_r2(&file)),
|
||||
Err(error) => Err(error.to_string()),
|
||||
}
|
||||
}
|
||||
}
|
106
src/values.rs
Normal file
106
src/values.rs
Normal file
@ -0,0 +1,106 @@
|
||||
use worker::Object;
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Hash, Eq)]
|
||||
pub enum Depth {
|
||||
Zero,
|
||||
One,
|
||||
#[default]
|
||||
Infinity,
|
||||
}
|
||||
|
||||
impl From<String> for Depth {
|
||||
fn from(m: String) -> Self {
|
||||
match m.to_ascii_uppercase().as_str() {
|
||||
"0" => Depth::Zero,
|
||||
"1" => Depth::One,
|
||||
"infinity" => Depth::Infinity,
|
||||
_ => Depth::Infinity,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Hash, Eq)]
|
||||
pub struct Range {
|
||||
pub start: Option<u32>,
|
||||
pub end: Option<u32>,
|
||||
}
|
||||
|
||||
impl From<Option<String>> for Range {
|
||||
fn from(line: Option<String>) -> Self {
|
||||
match line {
|
||||
None => Range {
|
||||
start: None,
|
||||
end: None,
|
||||
},
|
||||
Some(line) => Range::from(line),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<String> for Range {
|
||||
fn from(line: String) -> Self {
|
||||
if line.contains(";") {
|
||||
return Range {
|
||||
start: None,
|
||||
end: None,
|
||||
};
|
||||
}
|
||||
|
||||
line.split("-")
|
||||
.map(|v| v.parse::<u32>())
|
||||
.collect::<Result<Vec<u32>, _>>()
|
||||
.map_or(Range::from(None), |v| match v.len() {
|
||||
2 => Range {
|
||||
start: Some(v[0]),
|
||||
end: Some(v[1]),
|
||||
},
|
||||
_ => Range {
|
||||
start: None,
|
||||
end: None,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Hash, Eq)]
|
||||
pub enum Overwrite {
|
||||
#[default]
|
||||
True,
|
||||
False,
|
||||
}
|
||||
|
||||
impl From<String> for Overwrite {
|
||||
fn from(value: String) -> Self {
|
||||
match value.as_str() {
|
||||
"F" => Overwrite::False,
|
||||
"T" => Overwrite::True,
|
||||
_ => Overwrite::True,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Hash, Eq)]
|
||||
pub struct DavProperties {
|
||||
pub creation_date: Option<String>,
|
||||
pub display_name: Option<String>,
|
||||
pub get_content_language: Option<String>,
|
||||
pub get_content_length: Option<u64>,
|
||||
pub get_content_type: Option<String>,
|
||||
pub get_etag: Option<String>,
|
||||
pub get_last_modified: Option<String>,
|
||||
}
|
||||
|
||||
impl DavProperties {
|
||||
pub fn from_r2(file: &Object) -> DavProperties {
|
||||
let http_metedata = file.http_metadata();
|
||||
DavProperties {
|
||||
creation_date: Some(file.uploaded().to_string()),
|
||||
display_name: http_metedata.content_disposition,
|
||||
get_content_language: http_metedata.content_language,
|
||||
get_content_length: Some(file.size().into()),
|
||||
get_content_type: http_metedata.content_type,
|
||||
get_etag: Some(file.http_etag()),
|
||||
get_last_modified: None,
|
||||
}
|
||||
}
|
||||
}
|
90
src/xml.rs
Normal file
90
src/xml.rs
Normal file
@ -0,0 +1,90 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
pub struct XMLBuilder {
|
||||
name: String,
|
||||
value: Option<String>,
|
||||
attributes: Option<HashMap<String, String>>,
|
||||
elements: Vec<XMLBuilder>,
|
||||
}
|
||||
|
||||
impl XMLBuilder {
|
||||
pub fn new(
|
||||
name: String,
|
||||
attributes: Option<Vec<(String, String)>>,
|
||||
value: Option<String>,
|
||||
) -> XMLBuilder {
|
||||
XMLBuilder {
|
||||
name,
|
||||
value,
|
||||
attributes: attributes.map(|v| v.into_iter().collect()),
|
||||
elements: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn elem(
|
||||
&mut self,
|
||||
name: String,
|
||||
attributes: Option<Vec<(String, String)>>,
|
||||
value: Option<String>,
|
||||
) -> &mut XMLBuilder {
|
||||
let el = XMLBuilder::new(name, attributes, value);
|
||||
self.elements.push(el);
|
||||
self.elements.last_mut().unwrap()
|
||||
}
|
||||
|
||||
pub fn add(&mut self, element: XMLBuilder) {
|
||||
self.elements.push(element);
|
||||
}
|
||||
|
||||
pub fn build(&self) -> String {
|
||||
let mut xml = Vec::new();
|
||||
xml.push("<?xml version=\"1.0\" encoding=\"utf-8\"?>".to_string());
|
||||
xml.push(self.write_element(self));
|
||||
xml.join("")
|
||||
}
|
||||
|
||||
fn write_element(&self, element: &XMLBuilder) -> String {
|
||||
let mut xml = Vec::new();
|
||||
// attributes
|
||||
let mut attrs = Vec::new();
|
||||
if let Some(attributes) = &element.attributes {
|
||||
for (key, value) in attributes {
|
||||
attrs.push(format!("{}=\"{}\"", key, value));
|
||||
}
|
||||
}
|
||||
if !attrs.is_empty() {
|
||||
xml.push(format!("<{} {}>", element.name, attrs.join(" ")));
|
||||
} else {
|
||||
xml.push(format!("<{}>", element.name));
|
||||
}
|
||||
// value
|
||||
if let Some(value) = &element.value {
|
||||
xml.push(value.clone());
|
||||
}
|
||||
// elements
|
||||
for item in &element.elements {
|
||||
xml.push(self.write_element(item));
|
||||
}
|
||||
// end tag
|
||||
xml.push(format!("</{}>", element.name));
|
||||
xml.join("")
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::xml::XMLBuilder;
|
||||
|
||||
#[test]
|
||||
fn xml_build() {
|
||||
let mut xml = XMLBuilder::new("root".to_string(), None, None);
|
||||
xml.elem("child".to_string(), None, None)
|
||||
.elem("grandchild".to_string(), None, None)
|
||||
.add(XMLBuilder::new(
|
||||
"greatgrandchild".to_string(),
|
||||
None,
|
||||
Some("value".to_string()),
|
||||
));
|
||||
assert!(xml.build() == "<?xml version=\"1.0\" encoding=\"utf-8\"?><root><child><grandchild><greatgrandchild>value</greatgrandchild></grandchild></child></root>")
|
||||
}
|
||||
}
|
16
wrangler.toml
Normal file
16
wrangler.toml
Normal file
@ -0,0 +1,16 @@
|
||||
name = "r2-webdav"
|
||||
main = "build/worker/shim.mjs"
|
||||
compatibility_date = "2023-03-22"
|
||||
|
||||
[build]
|
||||
command = "cargo install -q worker-build && worker-build --release"
|
||||
|
||||
[[r2_buckets]]
|
||||
binding = 'webdav' # <~ valid JavaScript variable name
|
||||
bucket_name = 'webdav'
|
||||
|
||||
[vars]
|
||||
PROTOCOL = "r2"
|
||||
BUCKET_NAME = "webdav"
|
||||
USERNAME = "USERNAME"
|
||||
PASSWORD = "PASSWORD"
|
Loading…
Reference in New Issue
Block a user