WebDAV Plus is a feature-rich WebDAV client library for Dart and Flutter. Compared to other Dart WebDAV libraries, it offers more comprehensive protocol support with modern Dart idioms, including locking, ACL, versioning, search, and synchronization capabilities.
| Feature | WebDAV Plus | Other Dart Libraries |
|---|---|---|
| Core WebDAV (RFC 4918) | ✅ Supported | ✅ Partial |
| Locking (RFC 4918) | ✅ Supported | ❌ Limited |
| ACL (RFC 3744) | ✅ Supported | ❌ None |
| Versioning (RFC 3253) | ✅ Supported | ❌ None |
| Search (RFC 5323) | ✅ Basic | ❌ None |
| Sync (RFC 6578) | ✅ Basic | ❌ None |
| Quota (RFC 4331) | ✅ Supported | ❌ None |
| Streaming Support | ✅ Yes | ❌ Limited |
| Custom Auth Handlers | ✅ Extensible | ❌ Basic only |
- Core Operations: PROPFIND, PROPPATCH, MKCOL, DELETE, PUT, GET, COPY, MOVE
- Locking: Exclusive/shared locks, lock refresh, lock discovery
- Properties: Full property management with custom namespace support
- HTTP Basic Authentication with preemptive mode
- Domain authentication (NTLM-style username format)
- Extensible auth handler interface - easily integrate custom authentication schemes (OAuth, Kerberos, etc.)
- ACL (Access Control Lists): Read/modify permissions, principal management
- Quota: Query storage usage and availability
- Versioning (DeltaV): Check-in, check-out, version history, baselines
- Search: DASL-based content and property search
- Sync Collection: Efficient incremental synchronization
- Streaming downloads: Memory-efficient large file handling
- Streaming uploads: Upload from streams with progress tracking
- Compression: Built-in gzip/deflate support
- Connection reuse: HTTP client connection pooling
- Hierarchical exception system with specific error types
- Detailed error messages with HTTP status codes
- Automatic retry support for transient failures
Add to your pubspec.yaml:
dependencies:
webdav_plus: ^1.2.0Then run:
dart pub getFor a complete CRUD example, see example/simple.dart.
import 'package:webdav_plus/webdav_plus.dart';
void main() async {
// Create a client with credentials and base URL
final client = WebdavClient.withCredentials(
'username',
'password',
baseUrl: 'https://webdav.example.com/',
);
// List directory contents (paths are relative to baseUrl)
List<DavResource> resources = await client.list('/');
for (final resource in resources) {
print('${resource.isDirectory ? "[DIR]" : "[FILE]"} ${resource.name}');
}
// Upload a file
await client.put(
'/hello.txt',
Uint8List.fromList(utf8.encode('Hello, WebDAV!')),
);
// Download a file
Uint8List content = await client.get('/hello.txt');
print(utf8.decode(content));
// Create a directory
await client.createDirectory('/new-folder/');
// Move/Copy resources
await client.move('/hello.txt', '/new-folder/hello.txt');
await client.copy('/new-folder/hello.txt', '/backup.txt');
// Delete a resource
await client.delete('/backup.txt');
// Check if resource exists
bool exists = await client.exists('/new-folder/');
// Clean up
client.shutdown();
}// Basic client (no authentication)
final client = WebdavClient();
// With base URL only
final client = WebdavClient(baseUrl: 'https://webdav.example.com/');
// With credentials (challenge-response mode)
final client = WebdavClient.withCredentials('user', 'pass');
// With credentials and base URL
final client = WebdavClient.withCredentials(
'user',
'pass',
baseUrl: 'https://webdav.example.com/',
);
// With preemptive authentication (sends credentials immediately)
final client = WebdavClient.withCredentials('user', 'pass', isPreemptive: true);
// With compression enabled
final client = WebdavClient.withCompression();
// Fully configured
final client = WebdavClient.configured(
baseUrl: 'https://webdav.example.com/',
username: 'user',
password: 'pass',
isPreemptive: true,
compression: true,
);
// Set or change base URL at runtime
client.setBaseUrl('https://another-server.com/dav/');For memory-efficient handling of large files:
⚠️ Important: Streaming uploads (putFileStream,putStream) require preemptive authentication. The stream can only be read once, so if the server returns 401, it cannot be retried. UseisPreemptive: truewhen creating the client.
// Client must use preemptive auth for streaming uploads
final client = WebdavClient.withCredentials(
'user', 'pass',
baseUrl: 'https://webdav.example.com/',
isPreemptive: true, // Required for streaming uploads!
);
// Streaming download - returns Stream<List<int>>
Stream<List<int>> stream = await client.getStream(url);
// Download directly to file with progress
await client.downloadToFile(
url,
'/path/to/local/file.zip',
onProgress: (received, total) {
double progress = total > 0 ? (received / total * 100) : 0;
print('Download progress: ${progress.toStringAsFixed(1)}%');
},
);
// Streaming upload from file with progress
// contentType is optional - auto-detected from file extension
await client.putFileStream(
url,
File('/path/to/local/large-file.zip'),
onProgress: (sent, total) {
print('Upload progress: ${(sent / total * 100).toStringAsFixed(1)}%');
},
);
// Or specify contentType explicitly if needed
await client.putFileStream(
url,
File('/path/to/file'),
contentType: 'application/octet-stream',
);// Acquire an exclusive lock
String lockToken = await client.lock(url);
try {
// Perform operations while holding the lock
await client.put(url, newContent);
// Refresh the lock (extend timeout)
await client.refreshLock(url, lockToken, '3600'); // 1 hour
} finally {
// Always release the lock
await client.unlock(url, lockToken);
}
// Check if a resource is locked
bool isLocked = await client.isLocked(url);
// Discover existing locks
List<DavResource> lockedResources = await client.discoverLocks(url);// Get current ACL
DavAcl acl = await client.getAcl(url);
for (final ace in acl.aces) {
print('Principal: ${ace.principal}');
print('Privileges: ${ace.grantedPrivileges}');
}
// Check current user's privileges
List<String> privileges = await client.getCurrentUserPrivileges(url);
print('Your privileges: $privileges');
// Validate if user has specific privilege
bool canWrite = await client.hasPrivilege(url, 'write');// Put a resource under version control
await client.versionControl(url);
// Check out for editing
await client.checkout(url);
// Make modifications...
await client.put(url, updatedContent);
// Check in to create a new version
await client.checkin(url);
// Get version history
List<DavResource> versions = await client.versionsList(url);
for (final version in versions) {
print('Version: ${version.path} - ${version.modified}');
}
// Undo checkout
await client.uncheckout(url);DavQuota quota = await client.getQuota(url);
print('Used: ${quota.quotaUsedBytes} bytes');
print('Available: ${quota.quotaAvailableBytes} bytes');// Search for files by content
List<DavResource> results = await client.search(
baseUrl,
'contentcontains',
'important document',
);
// Search by property
List<DavResource> results = await client.search(
baseUrl,
'displayname',
'report',
);// Initial sync (get all resources)
(List<DavResource> resources, String syncToken) =
await client.syncCollection(url, null);
// Store syncToken for later...
// Incremental sync (get only changes since last sync)
(List<DavResource> changes, String newToken) =
await client.syncCollection(url, syncToken);Challenge-Response (default):
- Client sends request without credentials
- Server responds with 401 and WWW-Authenticate header
- Client retries with Authorization header
- More secure but requires extra round-trip
Preemptive:
- Client sends credentials with first request
- Faster (no extra round-trip)
- Required for streaming uploads (stream can't be replayed)
// Preemptive authentication
client.setCredentials('user', 'pass', isPreemptive: true);For Windows/NTLM environments:
client.setCredentialsWithDomain(
'username',
'password',
'DOMAIN',
'WORKSTATION',
isPreemptive: true,
);Implement the AuthenticationHandler interface for custom auth schemes:
class MyOAuthHandler implements AuthenticationHandler {
final String accessToken;
MyOAuthHandler(this.accessToken);
@override
String get schemeName => 'Bearer';
@override
Future<Map<String, String>> authenticate(
http.BaseRequest request,
http.StreamedResponse? response,
) async {
return {'Authorization': 'Bearer $accessToken'};
}
@override
bool canHandle(String scheme) => scheme.toLowerCase() == 'bearer';
@override
bool get requiresChallenge => false;
}
// Use the custom handler
client.setAuthenticationHandler(MyOAuthHandler(token), isPreemptive: true);try {
await client.get(url);
} on WebDAVNotFoundException catch (e) {
print('Resource not found: ${e.message}');
} on WebDAVAuthenticationException catch (e) {
print('Authentication failed: ${e.message}');
} on WebDAVForbiddenException catch (e) {
print('Access forbidden: ${e.message}');
} on WebDAVConflictException catch (e) {
print('Conflict (e.g., locked resource): ${e.message}');
} on WebDAVLockedException catch (e) {
print('Resource is locked: ${e.message}');
} on WebDAVInsufficientStorageException catch (e) {
print('Storage quota exceeded: ${e.message}');
} on WebDAVNetworkException catch (e) {
print('Network error: ${e.message}');
} on WebDAVException catch (e) {
print('WebDAV error: ${e.message} (HTTP ${e.statusCode})');
}| Method | Description |
|---|---|
list(url) |
List directory contents |
listWithDepth(url, depth) |
List with specific depth (0, 1, infinity) |
get(url) |
Download resource content |
getStream(url) |
Download as stream (memory-efficient) |
put(url, data) |
Upload content |
putStream(url, stream, length) |
Upload from stream |
delete(url) |
Delete resource |
createDirectory(url) |
Create collection (directory) |
move(src, dest) |
Move/rename resource |
copy(src, dest) |
Copy resource |
exists(url) |
Check if resource exists |
| Method | Description |
|---|---|
lock(url) |
Acquire exclusive lock |
unlock(url, token) |
Release lock |
refreshLock(url, token, timeout) |
Extend lock timeout |
isLocked(url) |
Check if locked |
discoverLocks(url) |
Find all locks |
| Method | Description |
|---|---|
getAcl(url) |
Get access control list |
setAcl(url, aces) |
Set access control list |
getCurrentUserPrivileges(url) |
Get current user's privileges |
hasPrivilege(url, privilege) |
Check specific privilege |
| Method | Description |
|---|---|
versionControl(url) |
Put under version control |
checkout(url) |
Check out for editing |
checkin(url) |
Check in new version |
uncheckout(url) |
Cancel checkout |
versionsList(url) |
Get version history |
- Dart SDK: >= 3.8.0
- Flutter: All platforms (iOS, Android, Web, Desktop)
- Standards: RFC 4918, RFC 3744, RFC 3253, RFC 5323, RFC 6578, RFC 4331
MIT License - see LICENSE for details.