Cloudsteading
Product image still needed. This listing has source documentation, but no reviewed screenshot yet.

r2-webdav

Access a Cloudflare R2 bucket through an authenticated WebDAV endpoint.

r2-webdav is a self-hosted Dropbox alternative built on Cloudflare (R2, Workers). Free tier eligible within limits. Inspect the source and license in the linked repository.

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

@abersheeran

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.
Check current pricing ↗
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.

Dropbox logoDropbox ↗

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 ↗
external SaaS target
varies
→ R2 + Workers

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 ↗
Public interface
Configured entry points1
r2-webdav
wrangler.toml
↓
App
r2-webdav
entry
Cloudflare Workers
Entrypoint: src/index.ts
↓

Configuration and workflow sources

Reviewed commit 172e24245065. Files were read as data; upstream applications and CI jobs were not executed.

Deployment configuration · 1 files
wrangler.toml ↗

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.

src/index.ts ↗
  • 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.

litmus · .github/workflows/litmus.yml ↗

Triggers: push, pull_request

litmus · no job dependencies declared

  1. Check out repositoryactions/checkout@v4
  2. Set up Node.jsactions/setup-node@v4
  3. Install dependenciesnpm ci
  4. Install litmussudo apt-get update sudo apt-get install -y litmus
  5. Configure development credentialscat <<EOF > .dev.vars USERNAME=${TEST_USERNAME} PASSWORD=${TEST_PASSWORD} EOF
  6. Start Wranglernohup npx wrangler dev --local --port "${WRANGLER_PORT}" > /tmp/wrangler.log 2>&1 & echo $! > /tmp/wrangler.pid
  7. Wait for Wranglerfor 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
  8. Run litmusTESTS="$TESTS" litmus -k "http://127.0.0.1:${WRANGLER_PORT}/" "${TEST_USERNAME}" "${TEST_PASSWORD}"
  9. Print Wrangler log on failurecat /tmp/wrangler.logCondition: failure()
  10. Stop Wranglerif [ -f /tmp/wrangler.pid ]; then kill "$(cat /tmp/wrangler.pid)" || true fiCondition: always()
package.json ↗
  • deploy: wrangler deploy

Full upstream document by @abersheeran · README.md · snapshot 172e242

r2-webdav

Deploy to Cloudflare Workers

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 →
No comments yet — be the first.