Source & license
Upstream license: Apache-2.0
License TL;DR
You can use, change and sell it, including in closed-source products. When sharing copies, include the license, keep required notices and mark changed files. It includes a contributor patent grant with conditions, but no trademark permission or warranty.
Explain Apache 2.0 in plain English →Summary of the main license. Separate packages and assets can have different terms.
Inspect repository ↗Read this project’s actual license ↗Repository owner
See the upstream repository for the original creator and contributors.
Maintain this project? Maintainer verification →Cloudflare hosting
Free tier eligible within limits
The documented r2-webdav deployment can use Cloudflare Free allowances for a small workload under the request, CPU and service-specific quotas below. This is conditional eligibility, not a measured zero-cost deployment; optional features, domains and external providers can add costs.
Hosting requirements
- Workers Free allows 100,000 requests per day shared across the account and 10 ms CPU per invocation; measure CPU-heavy authentication, parsing and rendering before assuming it fits.
- Use R2 Standard storage, at most 10 GB-month, 1 million Class A operations and 10 million Class B operations/month; provision an eligible billing-enabled R2 account.
- Use a small personal or team workload; domain registration and optional third-party providers are separate costs. Provision your own IDs, secrets and migrations.
Sources checked 01/10/2026
Repository snapshot: 172e242. Hosting eligibility reflects the deployment documentation and listed assumptions.
- dropbox ↗
Use Cloudflare Workers to provide a WebDav interface for Cloudflare R2.
- workers ↗
compatibility_date = "2023-10-16" main = "src/index.ts" name = "r2-webdav" compatibility_flags = ["nodejs_compat"] # Bind an R2 Bucket. Use R2 to store arbitrarily large blobs of data, such as files. # Docs: https://developers.cloudflare.com/r2/api/workers/workers-api-usage/ [[r2_buckets]] binding = "bucket" # <~ valid JavaScript variable name bucket_name = "webdav" # Docs: https://
- r2 ↗
ket. Use R2 to store arbitrarily large blobs of data, such as files. # Docs: https://developers.cloudflare.com/r2/api/workers/workers-api-usage/ [[r2_buckets]] binding = "bucket" # <~ valid JavaScript variable name bucket_name = "webdav" # Docs: https://developers.cloudflare.com/workers/observability/logs/workers-logs/#enable-workers-logs [observability] enabled = true head_sampling_rate = 1
- free-tier-eligible ↗
compatibility_date = "2023-10-16" main = "src/index.ts" name = "r2-webdav" compatibility_flags = ["nodejs_compat"] # Bind an R2 Bucket. Use R2 to store arbitrarily large blobs of data, such as files. # Docs: https://developers.cloudflare.com/r2/api/workers/workers-api-usage/ [[r2_buckets]] binding = "bucket" # <~ valid JavaScript variable name bucket_name = "webdav" # Docs: https://
- free-tier-eligible ↗
ket. Use R2 to store arbitrarily large blobs of data, such as files. # Docs: https://developers.cloudflare.com/r2/api/workers/workers-api-usage/ [[r2_buckets]] binding = "bucket" # <~ valid JavaScript variable name bucket_name = "webdav" # Docs: https://developers.cloudflare.com/workers/observability/logs/workers-logs/#enable-workers-logs [observability] enabled = true head_sampling_rate = 1
- free-tier-eligible ↗
up>1, 2, 3, 4</sup> | Duration | CPU time | | --- | --- | --- | --- | | **Free** | 100,000 per day | No charge for duration | 10 milliseconds of CPU time per invocation | | **Standard** | 10 million included per month <br> +$0.30 per additional million | No charge or limit for duration | 30 million CPU milliseconds included per month<br> +$0.02 per additional million CPU milliseconds<br><br> Max of [5 minutes of CPU time](https://developers.cloudflare.com/workers/platform/limits/#account-plan-limits) per invocation (default: 30 seconds)<br> Max of 15 minutes of CPU time per [Cron Trigger](https://developers.cloudflare.com/workers/configuration/cron-triggers/) or [Queue Consumer](https://developers.cloudflare.co
- free-tier-eligible ↗
infrequent access storage) for 1.1 GB, you will be billed for 2 GB. ### Free tier You can use the following amount of storage and operations each month for free. | | Free | | --- | --- | | Storage | 10 GB-month / month | | Class A Operations | 1 million requests / month | | Class B Operations | 10 million requests / month | | Egress (data transfer to Internet) | Free <sup>[1](#user-content-fn-1)</sup> | Caution The free tier only applies to Standard storage, and does not apply to Infrequent Access storage. ### Storage usage Storage is billed using gigabyte-month (GB-month) as the billing metric. A GB-month is calculated by averaging the *peak* storage per day over a billing period (30 days). For examp
- Apache-2.0 ↗
Copyright 2026 abersheeran Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the powe
- architecture ↗
compatibility_date = "2023-10-16" main = "src/index.ts" name = "r2-webdav" compatibility_flags = ["nodejs_compat"] # Bind an R2 Bucket. Use R2 to store arbitrarily large blobs of data, such as files. # Docs: https://developers.cloudflare.com/r2/api/workers/workers-api-usage/ [[r2_buckets]] binding = "bucket" # <~ valid JavaScript variable name bucket_name = "webdav" # Docs: https://
- architecture ↗
ket. Use R2 to store arbitrarily large blobs of data, such as files. # Docs: https://developers.cloudflare.com/r2/api/workers/workers-api-usage/ [[r2_buckets]] binding = "bucket" # <~ valid JavaScript variable name bucket_name = "webdav" # Docs: https://developers.cloudflare.com/workers/observability/logs/workers-logs/#enable-workers-logs [observability] enabled = true head_sampling_rate = 1
What it can replace
Compare the workflow you need. These mappings describe overlap; full feature parity requires a separate comparison.
Remote cloud-file upload/download from compatible WebDAV clients; Dropbox sync clients, collaboration, share-link UI and managed service operation are excluded.
See supporting source ↗How it works
The shape of r2-webdav on Cloudflare, and how it stacks up against the rented tools it replaces.
Architecture
Diagram of deployment declarations at the reviewed commit. Each app has its own entrypoint; declared resources do not prove runtime calls. Follow file and line sources below.
View upstream source ↗Configuration and workflow sources
Reviewed commit 172e24245065. Files were read as data; upstream applications and CI jobs were not executed.
Deployment configuration · 1 files
Cloudflare Workers · compatibility 2023-10-16
r2-webdav · default
Entrypoint: src/index.ts
bucket→ R2
Named environments are separate deployments. Bindings are shown only where declared. Configured routes are URL patterns, not verified application endpoints.
Runtime source · handlers, binding usage and workflow steps
Observed TypeScript/JavaScript declarations from Worker entrypoints and resolved relative imports. Calls and workflow steps may run conditionally; their listed order is not a proven end-to-end request flow. Router declarations may be mounted under a prefix or may not be registered. This shows code wiring, not a successful deployment or runtime test. Dynamic wiring, aliases and generated code may not resolve.
- L1604 · fetch handler exported · references USERNAME, PASSWORD · calls is_authorized, request.headers.get, dispatch_handler, response.headers.set, SUPPORT_METHODS.join, join
- L22 · listAll calls (conditional paths may differ): bucket.list
- L109 · escapeXml calls (conditional paths may differ): replaceAll, value.replaceAll
- L118 · getResourceHref calls (conditional paths may differ): join, map, href.split, encodeURIComponent, encodeHrefPath
- L135 · decodeResourcePath calls (conditional paths may differ): pathname.slice, resourcePath.endsWith, resourcePath.slice, join, map, resourcePath.split, decodeURIComponent
- L153 · getParentPath calls (conditional paths may differ): resourcePath.endsWith, resourcePath.slice, join, slice, normalizedPath.split
- L158 · hasCollectionResource calls (conditional paths may differ): bucket.head, bucket.list
- L175 · parseDestinationPath calls (conditional paths may differ): decodeResourcePath
- L187 · isSameOrDescendantPath calls (conditional paths may differ): destinationPath.startsWith
- L197 · createdResponse calls (conditional paths may differ): responseHeaders.set, getResourceHref
- L211 · renderDavProperty calls (conditional paths may differ): RAW_XML_DAV_PROPERTIES.has, escapeXml
- L216 · serializeNodeChildren calls (conditional paths may differ): child.toString
- L224 · getDeadPropertyKey calls (conditional paths may differ): encodeURIComponent
- L228 · getDeadProperty calls (conditional paths may differ): getDeadPropertyKey, JSON.parse
- L240 · getDeadProperties calls (conditional paths may differ): map, filter, Object.entries, key.startsWith, JSON.parse
- L249 · renderPropertyElement calls (conditional paths may differ): escapeXml
- L260 · renderEmptyPropertyElement calls (conditional paths may differ): escapeXml
- L271 · getElementProperty calls (conditional paths may differ): serializeNodeChildren
- L283 · parseXmlDocument calls (conditional paths may differ): parseFromString, errors.push
- L298 · getChildElements calls (conditional paths may differ): children.push
- L308 · parsePropfindRequest calls (conditional paths may differ): body.trim, parseXmlDocument, document.documentElement.localName.toLowerCase, getChildElements, propfindChildren.some, child.localName.toLowerCase, propfindChildren.find, map, properties.some
- L337 · parseProppatchRequest calls (conditional paths may differ): parseXmlDocument, document.documentElement.localName.toLowerCase, getChildElements, actionElement.localName.toLowerCase, find, child.localName.toLowerCase, getElementProperty, operations.push
- L363 · getSupportedLock calls (conditional paths may differ): join
- L380 · normalizeLockToken calls (conditional paths may differ): replace, lockToken.trim
- L387 · normalizeLockDetails calls (conditional paths may differ): Number, Number.isFinite, Date.now
- L407 · getLockDetails calls (conditional paths may differ): JSON.parse, Array.isArray, parsed.flatMap, normalizeLockDetails, Number
- L441 · getLockDiscovery calls (conditional paths may differ): Array.isArray, join, lockDetailList.map, escapeXml
- L459 · withLockMetadata calls (conditional paths may differ): Array.isArray, stripLockMetadata, JSON.stringify
- L473 · getPreservedCustomMetadata calls (conditional paths may differ): getLockDetails, stripLockMetadata, withLockMetadata
- L481 · isProtectedProperty calls (conditional paths may differ): pop, propName.split, LOCK_METADATA_KEYS.includes
- L488 · isValidXmlTagName calls (conditional paths may differ): test
- L492 · parseTimeout calls (conditional paths may differ): Date.now, map, timeoutHeader.split, value.trim, item.toLowerCase, Number, item.match, Number.isFinite, Math.min
- L524 · getRequestLockTokens calls (conditional paths may differ): request.headers.get, lockTokens.push, normalizeLockToken, ifHeader.matchAll
- L544 · hasAlwaysFalseIfCondition calls (conditional paths may differ): request.headers.get, ifHeader.includes
- L560 · extractLockOwner calls (conditional paths may differ): body.match, owner.trim
- L570 · fromR2Object calls (conditional paths may differ): toUTCString, getSupportedLock, getLockDetails, object.uploaded.toUTCString, object.size.toString, getLockDiscovery, lockDetails.map, getResourceHref
- L610 · getLivePropertyValue calls (conditional paths may differ): fromR2Object
- L617 · renderPropstat calls (conditional paths may differ): properties.join
- L630 · make_resource_path calls (conditional paths may differ): decodeResourcePath
- L634 · assertLockPermission calls (conditional paths may differ): hasAlwaysFalseIfCondition, getRequestLockTokens, join, slice, current.split, candidates.push, bucket.head, filter, getLockDetails, lockDetails.some, lockTokens.includes
Environment references: env.USERNAME · env.PASSWORD
Build and deployment pipeline · 1 GitHub Actions workflows
Repository CI declarations, separate from runtime request processing. Job dependencies and conditions are shown as written; long commands are shortened with an ellipsis; a workflow file does not prove a recent successful run.
Triggers: push, pull_request
litmus · no job dependencies declared
- Check out repository
actions/checkout@v4 - Set up Node.js
actions/setup-node@v4 - Install dependencies
npm ci - Install litmus
sudo apt-get update sudo apt-get install -y litmus - Configure development credentials
cat <<EOF > .dev.vars USERNAME=${TEST_USERNAME} PASSWORD=${TEST_PASSWORD} EOF - Start Wrangler
nohup npx wrangler dev --local --port "${WRANGLER_PORT}" > /tmp/wrangler.log 2>&1 & echo $! > /tmp/wrangler.pid - Wait for Wrangler
for attempt in $(seq 1 30); do if curl --fail --silent --show-error --user "${TEST_USERNAME}:${TEST_PASSWORD}" "http://127.0.0.1:${WRANGLER_PORT}/" > /dev/null; then exit 0 fi sleep 1 done cat /tmp/wrangler.log exit 1 - Run litmus
TESTS="$TESTS" litmus -k "http://127.0.0.1:${WRANGLER_PORT}/" "${TEST_USERNAME}" "${TEST_PASSWORD}" - Print Wrangler log on failure
cat /tmp/wrangler.logCondition: failure() - Stop Wrangler
if [ -f /tmp/wrangler.pid ]; then kill "$(cat /tmp/wrangler.pid)" || true fiCondition: always()
deploy: wrangler deploy
Repository README
View original on GitHub ↗Full upstream document by @abersheeran · README.md · snapshot 172e242
r2-webdav
Use Cloudflare Workers to provide a WebDav interface for Cloudflare R2.
Currently the server advertises WebDAV Class 1 and Class 2 (LOCK/UNLOCK) support.
Usage
Change wrangler.toml to your own.
[[r2_buckets]]
binding = 'bucket' # <~ valid JavaScript variable name, don't change this
bucket_name = 'webdav'
Then use wrangler to deploy.
wrangler deploy
wrangler secret put USERNAME
wrangler secret put PASSWORD
Development
With wrangler, you can run and deploy your Worker with the following commands:
# 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
Test
Use litmus to test.
GitHub Actions runs the basic, copymove, props, and locks litmus suites against wrangler dev --local.
The http suite is currently excluded because local Workers runs still time out on the interim Expect: 100-continue response check.
Frequently asked about r2-webdav
What is r2-webdav?+
r2-webdav is a self-hosted Dropbox alternative built on the Cloudflare developer platform. Access a Cloudflare R2 bucket through an authenticated WebDAV endpoint.
What does r2-webdav replace?+
r2-webdav is listed as an alternative to Dropbox. Compare the features and tradeoffs before migrating.
What Cloudflare primitives does r2-webdav use?+
r2-webdav is built on R2, Workers.
How much does r2-webdav cost to run?+
The documented r2-webdav deployment can use Cloudflare Free allowances for a small workload under the request, CPU and service-specific quotas below. This is conditional eligibility, not a measured zero-cost deployment; optional features, domains and external providers can add costs. Workers Free allows 100,000 requests per day shared across the account and 10 ms CPU per invocation; measure CPU-heavy authentication, parsing and rendering before assuming it fits. Use R2 Standard storage, at most 10 GB-month, 1 million Class A operations and 10 million Class B operations/month; provision an eligible billing-enabled R2 account. Use a small personal or team workload; domain registration and optional third-party providers are separate costs. Provision your own IDs, secrets and migrations. Check current Cloudflare pricing before deploying.
Is r2-webdav open source?+
The upstream repository declares the Apache-2.0 license. Read its terms at https://raw.githubusercontent.com/abersheeran/r2-webdav/172e24245065fa3b84538a40a0cbe398007290fc/LICENSE. Source code and contributor credit are available at https://github.com/abersheeran/r2-webdav.

Discussion · 0
sign in to comment →