WebDAV filesystem implementation for the absfs ecosystem.
webdavfs provides bidirectional WebDAV support for the absfs ecosystem:
- Client Mode - Access remote WebDAV servers through the
absfs.FileSysteminterface - Server Mode - Expose any
absfs.FileSystemas a WebDAV server
This enables seamless integration with any WebDAV-compliant server (Nextcloud, ownCloud, Apache mod_dav, nginx, etc.) and allows serving any absfs filesystem (memfs, osfs, s3fs, unionfs, etc.) over WebDAV.
import "github.com/absfs/webdavfs"
// Connect to a WebDAV server
fs, err := webdavfs.New(&webdavfs.Config{
URL: "https://webdav.example.com/remote.php/dav/files/user/",
Username: "user",
Password: "password",
})
if err != nil {
log.Fatal(err)
}
defer fs.Close()
// Use standard absfs operations
file, _ := fs.Open("/documents/report.txt")import (
"github.com/absfs/memfs"
"github.com/absfs/webdavfs"
)
// Create any absfs.FileSystem
fs, _ := memfs.NewFS()
// Serve it via WebDAV
server := webdavfs.NewServer(fs, &webdavfs.ServerConfig{
Prefix: "/webdav",
Auth: &webdavfs.BasicAuth{
Realm: "My Server",
Validator: func(user, pass string) bool {
return user == "admin" && pass == "secret"
},
},
})
http.ListenAndServe(":8080", server)webdavfs follows the Direct Backend Integration pattern used by other absfs network filesystem implementations like sftpfs and s3fs. The implementation consists of:
- FileSystem struct - Holds WebDAV client connection and configuration
- File wrapper - Adapts WebDAV file operations to the
absfs.Fileinterface - Protocol mapping - Translates absfs operations to WebDAV HTTP methods
┌─────────────────────┐
│ absfs Application │
│ (uses absfs API) │
└──────────┬──────────┘
│ absfs.FileSystem interface
▼
┌─────────────────────┐
│ webdavfs │
│ FileSystem struct │
└──────────┬──────────┘
│ WebDAV HTTP protocol
▼
┌─────────────────────┐
│ WebDAV Server │
│ (remote storage) │
└─────────────────────┘
The package implements the complete absfs.FileSystem interface hierarchy:
OpenFile(name string, flag int, perm os.FileMode) (File, error)→ WebDAV GET/PUT/MKCOLMkdir(name string, perm os.FileMode) error→ WebDAV MKCOLRemove(name string) error→ WebDAV DELETERename(oldpath, newpath string) error→ WebDAV MOVEStat(name string) (os.FileInfo, error)→ WebDAV PROPFINDChmod(name string, mode os.FileMode) error→ WebDAV PROPPATCH (limited support)Chtimes(name string, atime, mtime time.Time) error→ WebDAV PROPPATCHChown(name string, uid, gid int) error→ WebDAV PROPPATCH (limited support)
Separator()→ Returns '/'ListSeparator()→ Returns ':'Chdir(dir string) error→ Local state trackingGetwd() (string, error)→ Local state trackingTempDir() string→ Returns server-specific temp pathOpen(name string) (File, error)→ Convenience wrapper for OpenFileCreate(name string) (File, error)→ Convenience wrapper for OpenFileMkdirAll(name string, perm os.FileMode) error→ Recursive MKCOLRemoveAll(path string) error→ Recursive DELETETruncate(name string, size int64) error→ WebDAV partial PUT
The File wrapper implements absfs.File (which extends io.Reader, io.Writer, io.Seeker, io.Closer):
Read(b []byte) (int, error)→ HTTP GET with rangeWrite(b []byte) (int, error)→ HTTP PUT with bufferingClose() error→ Flush writes, close connectionSeek(offset int64, whence int) (int64, error)→ Update position trackingStat() (os.FileInfo, error)→ WebDAV PROPFIND
ReadAt(b []byte, off int64) (int, error)→ HTTP GET with Range headerWriteAt(b []byte, off int64) (int, error)→ HTTP PUT with Content-RangeReaddir(n int) ([]os.FileInfo, error)→ WebDAV PROPFIND with depth=1Readdirnames(n int) ([]string, error)→ Wrapper around ReaddirTruncate(size int64) error→ WebDAV partial PUTSync() error→ Flush buffered writes
| absfs Operation | WebDAV Method | Description |
|---|---|---|
| Stat | PROPFIND | Get file/directory properties |
| Open (read) | GET | Download file content |
| Create/Write | PUT | Upload file content |
| Mkdir | MKCOL | Create directory |
| Remove | DELETE | Delete file/directory |
| Rename | MOVE | Rename/move resource |
| Chtimes | PROPPATCH | Modify modification time |
| Readdir | PROPFIND (Depth: 1) | List directory contents |
Standard DAV properties accessed via PROPFIND:
displayname→ File namegetcontentlength→ File sizegetlastmodified→ Modification timeresourcetype→ File vs directory detectiongetetag→ Cache validationgetcontenttype→ MIME type
WebDAV is a loosely defined standard with varying server implementations:
-
Permissions - Not all servers support Unix permissions via PROPPATCH
Chmodmay be a no-op on some serversChowntypically unsupported in most WebDAV servers
-
Atomic Operations - Limited atomicity guarantees
- No native locking in basic WebDAV (requires WebDAV Locking extension)
- Concurrent writes may result in race conditions
-
Performance - Network latency considerations
- Each operation is an HTTP request
- Directory listings can be expensive (recursive PROPFIND)
- Consider using caching wrappers like
corfsfor read-heavy workloads
-
Partial Updates - Server-dependent support
WriteAtandTruncaterequire Content-Range support- Some servers may require full file replacement
- Authentication - Support for HTTP Basic, Digest, and Bearer tokens
- TLS/HTTPS - Strongly recommended for production use
- Credentials - Stored in memory, consider using credential helpers
- Path Traversal - All paths sanitized before HTTP requests
import "github.com/absfs/webdavfs"
// Connect to WebDAV server
fs, err := webdavfs.New(&webdavfs.Config{
URL: "https://webdav.example.com/remote.php/dav/files/user/",
Username: "user",
Password: "password",
})
if err != nil {
log.Fatal(err)
}
defer fs.Close()
// Use standard absfs operations
file, err := fs.Create("documents/report.txt")
if err != nil {
log.Fatal(err)
}
defer file.Close()
_, err = file.Write([]byte("Hello WebDAV!"))import (
"github.com/absfs/webdavfs"
"github.com/absfs/corfs"
"github.com/absfs/memfs"
)
// Remote WebDAV as primary
remote, _ := webdavfs.New(&webdavfs.Config{...})
// Local memory cache
cache := memfs.NewFS()
// Wrap with cache-on-read
fs := corfs.New(remote, cache)
// First read hits remote, subsequent reads use cache
data, _ := fs.ReadFile("large-file.bin")import (
"github.com/absfs/webdavfs"
"github.com/absfs/cowfs"
)
// WebDAV base layer (read-only in practice)
base, _ := webdavfs.New(&webdavfs.Config{...})
// Local overlay for writes
overlay := os.DirFS("/tmp/overlay")
// Writes go to overlay, reads fall through to WebDAV
fs := cowfs.New(overlay, base)
// Modify without affecting remote until explicit sync
fs.WriteFile("config.yml", data, 0644)import (
"github.com/absfs/webdavfs"
"github.com/absfs/sftpfs"
"github.com/absfs/s3fs"
)
// Access same logical filesystem via different protocols
webdav, _ := webdavfs.New(&webdavfs.Config{URL: "https://..."})
sftp, _ := sftpfs.New(&sftpfs.Config{Host: "..."})
s3, _ := s3fs.New(&s3fs.Config{Bucket: "..."})
// Application code uses absfs.FileSystem interface
// Can switch between protocols transparently
var fs absfs.FileSystem = webdavExpected package structure:
webdavfs/
├── README.md # This file
├── FUZZING.md # Fuzz testing documentation
├── go.mod # Module definition
├── webdavfs.go # FileSystem implementation
├── file.go # File wrapper implementation
├── client.go # WebDAV HTTP client
├── properties.go # WebDAV property parsing
├── config.go # Configuration structs
├── errors.go # Error handling
├── webdavfs_test.go # Integration tests
└── fuzz_test.go # Fuzz tests
type FileSystem struct {
client *webdavClient
root string
cwd string
}
func New(config *Config) (*FileSystem, error)
func (fs *FileSystem) OpenFile(...) (absfs.File, error)
func (fs *FileSystem) Mkdir(...) error
// ... other Filer methodstype File struct {
fs *FileSystem
path string
mode int
offset int64
info os.FileInfo
buffer *bytes.Buffer // For write buffering
modified bool
}
func (f *File) Read(b []byte) (int, error)
func (f *File) Write(b []byte) (int, error)
// ... other File methodstype webdavClient struct {
httpClient *http.Client
baseURL *url.URL
auth authProvider
}
func (c *webdavClient) propfind(...) (*multistatus, error)
func (c *webdavClient) get(...) (io.ReadCloser, error)
func (c *webdavClient) put(...) error
func (c *webdavClient) mkcol(...) error
func (c *webdavClient) delete(...) error
func (c *webdavClient) move(...) error- Mock HTTP server using
httptest - Test each absfs method with various WebDAV responses
- Error handling for malformed XML, network errors
- Path sanitization and encoding
- Comprehensive fuzz testing for security and robustness
- Tests XML parsing, path encoding, HTTP response handling, authentication headers, and property values
- Helps discover edge cases and potential security vulnerabilities
- See FUZZING.md for detailed documentation
- Real WebDAV server (docker container)
- Full absfs compliance test suite
- Concurrent access patterns
- Large file handling
Against common WebDAV servers:
- Apache mod_dav
- nginx with ngx_http_dav_module
- Nextcloud
- ownCloud
- SabreDAV
The server functionality allows exposing any absfs.FileSystem as a WebDAV server. This is useful for:
- Serving in-memory filesystems for testing
- Creating WebDAV gateways to S3, SFTP, or other storage backends
- Implementing virtual filesystems with WebDAV access
┌─────────────────────┐
│ WebDAV Client │
│ (any HTTP client) │
└──────────┬──────────┘
│ WebDAV HTTP protocol
▼
┌─────────────────────┐
│ webdavfs.Server │
│ (HTTP handler) │
└──────────┬──────────┘
│ webdav.FileSystem interface
▼
┌─────────────────────┐
│ ServerFileSystem │
│ (adapter) │
└──────────┬──────────┘
│ absfs.FileSystem interface
▼
┌─────────────────────┐
│ Any absfs FS │
│ (memfs, osfs, etc) │
└─────────────────────┘
type ServerConfig struct {
// Prefix is the URL path prefix (e.g., "/webdav")
Prefix string
// Auth is an optional authentication provider
Auth AuthProvider
// Logger logs each request
Logger func(r *http.Request, err error)
// LockSystem for WebDAV locking (default: in-memory)
LockSystem webdav.LockSystem
}server := webdavfs.NewServer(fs, &webdavfs.ServerConfig{
Auth: &webdavfs.BasicAuth{
Realm: "My WebDAV Server",
Validator: func(username, password string) bool {
return username == "admin" && password == "secret"
},
},
})server := webdavfs.NewServer(fs, &webdavfs.ServerConfig{
Auth: &webdavfs.BearerAuth{
Realm: "API",
Validator: func(token string) bool {
return token == "valid-api-token"
},
},
})Implement the AuthProvider interface:
type AuthProvider interface {
Authenticate(w http.ResponseWriter, r *http.Request) bool
}import (
"github.com/absfs/memfs"
"github.com/absfs/osfs"
"github.com/absfs/unionfs"
"github.com/absfs/webdavfs"
)
// Create layers
base, _ := osfs.NewFS()
overlay, _ := memfs.NewFS()
// Create union
ufs, _ := unionfs.New(overlay, base)
// Serve via WebDAV
server := webdavfs.NewServer(ufs, nil)
http.ListenAndServe(":8080", server)import (
"github.com/absfs/s3fs"
"github.com/absfs/webdavfs"
)
// Connect to S3
s3, _ := s3fs.New(&s3fs.Config{
Bucket: "my-bucket",
Region: "us-east-1",
})
// Serve S3 bucket via WebDAV
server := webdavfs.NewServer(s3, &webdavfs.ServerConfig{
Prefix: "/s3",
})
http.ListenAndServe(":8080", server)server := webdavfs.NewServer(fs, &webdavfs.ServerConfig{
Logger: func(r *http.Request, err error) {
if err != nil {
log.Printf("WebDAV ERROR %s %s: %v", r.Method, r.URL.Path, err)
} else {
log.Printf("WebDAV %s %s", r.Method, r.URL.Path)
}
},
})| HTTP Method | WebDAV Operation | Description |
|---|---|---|
| GET | Download | Download file content |
| PUT | Upload | Upload file content |
| DELETE | Remove | Delete file or directory |
| MKCOL | Mkdir | Create directory |
| COPY | Copy | Copy resource |
| MOVE | Rename | Move/rename resource |
| PROPFIND | Stat/List | Get properties or list directory |
| PROPPATCH | SetProps | Modify properties |
| LOCK | Lock | Lock resource (in-memory by default) |
| UNLOCK | Unlock | Unlock resource |
| OPTIONS | Discover | WebDAV capability discovery |
- absfs - Core filesystem abstraction
- memfs - In-memory filesystem
- osfs - OS filesystem wrapper
- sftpfs - SFTP protocol access
- s3fs - S3-compatible object storage
- corfs - Cache-on-read wrapper
- cowfs - Copy-on-write layers
- unionfs - Union/overlay filesystem
- golang.org/x/net/webdav - WebDAV server implementation (used by Server mode)
- github.com/studio-b12/gowebdav - WebDAV client library
This implementation follows absfs ecosystem principles:
- Interface Compliance - Full implementation of
absfs.FileSystem - Composition-Friendly - Works seamlessly with other absfs wrappers
- Minimal Dependencies - Only essential WebDAV protocol libraries
- Error Transparency - WebDAV errors wrapped in absfs error types
- Zero Configuration - Sensible defaults, minimal required config
- Test Coverage - Comprehensive tests against real servers
MIT License - see LICENSE file for details.
Contributions welcome! Please ensure:
- All
absfs.FileSystemmethods implemented - Tests pass against multiple WebDAV servers
- Documentation updated for new features
- Error handling follows absfs patterns