├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── lib ├── src │ ├── client.dart │ └── file.dart └── webdav.dart ├── pubspec.yaml └── test └── webdav_test.dart /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .DS_Store 3 | .dart_tool/ 4 | .pub/ 5 | .idea/ 6 | build 7 | .packages 8 | pubspec.lock 9 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/timestee/dart-webdav/9caeae0c0191b683dc89f147c6f045174d64e813/CHANGELOG.md -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | **Maintainers wanted!** 2 | ---- 3 | # dart-webdav 4 | A Easy WebDAV Client in Dart 5 | 6 | ## Features 7 | * Basic authentication 8 | * Creating directories, removing directories and files 9 | * Uploading and downloading files 10 | * Directory listing 11 | 12 | ## TODO 13 | * testcase 14 | -------------------------------------------------------------------------------- /lib/src/client.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | import 'dart:io'; 3 | import 'dart:typed_data'; 4 | 5 | import 'package:retry/retry.dart'; 6 | 7 | import 'file.dart'; 8 | 9 | class WebDavException implements Exception { 10 | String cause; 11 | int statusCode; 12 | 13 | WebDavException(this.cause, this.statusCode); 14 | } 15 | 16 | class Client { 17 | final HttpClient httpClient = new HttpClient(); 18 | late String _baseUrl; 19 | String _cwd = '/'; 20 | 21 | /// Construct a new [Client]. 22 | /// [path] will should be the root path you want to access. 23 | Client( 24 | String host, 25 | String user, 26 | String password, { 27 | String? path, 28 | String? protocol, 29 | int? port, 30 | }) : assert((host.startsWith('https://') || 31 | host.startsWith('http://') || 32 | protocol != null)) { 33 | _baseUrl = (protocol != null 34 | ? '$protocol://$host${port != null ? ':$port' : ''}' 35 | : host) + 36 | (path ?? ''); 37 | this.httpClient.addCredentials( 38 | Uri.parse(_baseUrl), '', HttpClientBasicCredentials(user, password)); 39 | } 40 | 41 | /// get url from given [path] 42 | String getUrl(String path) => 43 | path.startsWith('/') ? _baseUrl + path : '$_baseUrl$_cwd$path'; 44 | 45 | /// change current dir to the given [path], you should make sure the dir exist 46 | void cd(String path) { 47 | path = path.trim(); 48 | if (path.isEmpty) { 49 | return; 50 | } 51 | List tmp = path.split("/"); 52 | tmp.removeWhere((value) => value == null || value == ''); 53 | String strippedPath = tmp.join('/') + '/'; 54 | if (strippedPath == '/') { 55 | _cwd = strippedPath; 56 | } else if (path.startsWith("/")) { 57 | _cwd = '/' + strippedPath; 58 | } else { 59 | _cwd += strippedPath; 60 | } 61 | } 62 | 63 | /// send the request with given [method] and [path] 64 | /// 65 | Future _send( 66 | String method, String path, List expectedCodes, 67 | {Uint8List? data, Map? headers}) async { 68 | return await retry( 69 | () => this 70 | .__send(method, path, expectedCodes, data: data, headers: headers), 71 | retryIf: (e) => e is WebDavException, 72 | maxAttempts: 5); 73 | } 74 | 75 | /// send the request with given [method] and [path] 76 | Future __send( 77 | String method, String path, List expectedCodes, 78 | {Uint8List? data, Map? headers}) async { 79 | String url = this.getUrl(path); 80 | print("[webdav] http send with method:$method path:$path url:$url"); 81 | 82 | HttpClientRequest request = 83 | await this.httpClient.openUrl(method, Uri.parse(url)); 84 | request 85 | ..followRedirects = false 86 | ..persistentConnection = true; 87 | 88 | if (data != null) { 89 | request.add(data); 90 | } 91 | if (headers != null) { 92 | headers.forEach((k, v) => request.headers.add(k, v)); 93 | } 94 | 95 | HttpClientResponse response = await request.close(); 96 | if (!expectedCodes.contains(response.statusCode)) { 97 | throw WebDavException( 98 | "operation failed method:$method " 99 | "path:$path exceptionCodes:$expectedCodes " 100 | "statusCode:${response.statusCode}", 101 | response.statusCode); 102 | } 103 | return response; 104 | } 105 | 106 | /// make a dir with [path] under current dir 107 | Future mkdir(String path, [bool safe = true]) { 108 | List expectedCodes = [201]; 109 | if (safe) { 110 | expectedCodes.addAll([301, 405]); 111 | } 112 | return this._send('MKCOL', path, expectedCodes); 113 | } 114 | 115 | /// just like mkdir -p 116 | Future mkdirs(String path) async { 117 | path = path.trim(); 118 | List dirs = path.split("/"); 119 | dirs.removeWhere((value) => value == ''); 120 | if (dirs.isEmpty) { 121 | return; 122 | } 123 | if (path.startsWith("/")) { 124 | dirs[0] = '/' + dirs[0]; 125 | } 126 | String oldCwd = _cwd; 127 | try { 128 | for (String dir in dirs) { 129 | try { 130 | await this.mkdir(dir, true); 131 | } catch (e) { 132 | } finally { 133 | this.cd(dir); 134 | } 135 | } 136 | } catch (e) {} finally { 137 | this.cd(oldCwd); 138 | } 139 | } 140 | 141 | /// remove dir with given [path] 142 | Future rmdir(String path, [bool safe = true]) async { 143 | path = path.trim(); 144 | if (!path.endsWith('/')) { 145 | // Apache is unhappy when directory to be deleted 146 | // does not end with '/' 147 | path += '/'; 148 | } 149 | List expectedCodes = [204]; 150 | if (safe) { 151 | expectedCodes.addAll([204, 404]); 152 | } 153 | await this._send('DELETE', path, expectedCodes); 154 | } 155 | 156 | /// remove dir with given [path] 157 | Future delete(String path) async { 158 | await this._send('DELETE', path, [204]); 159 | } 160 | 161 | /// upload a new file with [localData] as content to [remotePath] 162 | Future _upload(Uint8List localData, String remotePath) async { 163 | await this._send('PUT', remotePath, [200, 201, 204], data: localData); 164 | } 165 | 166 | /// upload a new file with [localData] as content to [remotePath] 167 | Future upload(Uint8List data, String remotePath) async { 168 | await this._upload(data, remotePath); 169 | } 170 | 171 | /// upload local file [path] to [remotePath] 172 | Future uploadFile(String path, String remotePath) async { 173 | await this._upload(await File(path).readAsBytes(), remotePath); 174 | } 175 | 176 | /// download [remotePath] to local file [localFilePath] 177 | Future download(String remotePath, String localFilePath) async { 178 | HttpClientResponse response = await this._send('GET', remotePath, [200]); 179 | await response.pipe(new File(localFilePath).openWrite()); 180 | } 181 | 182 | /// download [remotePath] and store the response file contents to String 183 | Future downloadToBinaryString(String remotePath) async { 184 | HttpClientResponse response = await this._send('GET', remotePath, [200]); 185 | return response.transform(utf8.decoder).join(); 186 | } 187 | 188 | /// list the directories and files under given [remotePath] 189 | Future> ls({String? path, int depth = 1}) async { 190 | Map userHeader = {"Depth": depth}; 191 | HttpClientResponse response = await this 192 | ._send('PROPFIND', path ?? '/', [207, 301], headers: userHeader); 193 | if (response.statusCode == 301) { 194 | return this.ls(path: response.headers.value('location')); 195 | } 196 | return treeFromWebDavXml(await response.transform(utf8.decoder).join()); 197 | } 198 | } 199 | -------------------------------------------------------------------------------- /lib/src/file.dart: -------------------------------------------------------------------------------- 1 | import 'package:xml/xml.dart' as xml; 2 | 3 | class FileInfo { 4 | String path; 5 | String size; 6 | String modificationTime; 7 | DateTime creationTime; 8 | String contentType; 9 | 10 | FileInfo(this.path, this.size, this.modificationTime, this.creationTime, 11 | this.contentType); 12 | 13 | // Returns the decoded name of the file / folder without the whole path 14 | String get name { 15 | if (this.isDirectory) { 16 | return Uri.decodeFull( 17 | this.path.substring(0, this.path.lastIndexOf("/")).split("/").last); 18 | } 19 | 20 | return Uri.decodeFull(this.path.split("/").last); 21 | } 22 | 23 | bool get isDirectory => this.path.endsWith("/"); 24 | 25 | @override 26 | String toString() { 27 | return 'FileInfo{name: $name, isDirectory: $isDirectory ,path: $path, size: $size, modificationTime: $modificationTime, creationTime: $creationTime, contentType: $contentType}'; 28 | } 29 | } 30 | 31 | /// get filed [name] from the property node 32 | String? prop(dynamic prop, String name, [String? defaultVal]) { 33 | if (prop is Map) { 34 | final val = prop['D:' + name]; 35 | if (val == null) { 36 | return defaultVal; 37 | } 38 | return val; 39 | } 40 | return defaultVal; 41 | } 42 | 43 | List treeFromWebDavXml(String xmlStr) { 44 | // Initialize a list to store the FileInfo Objects 45 | List tree = new List.empty(growable: true); 46 | 47 | // parse the xml using the xml.parse method 48 | var xmlDocument = xml.XmlDocument.parse(xmlStr); 49 | 50 | // Iterate over the response to find all folders / files and parse the information 51 | findAllElementsFromDocument(xmlDocument, "response").forEach((response) { 52 | var davItemName = findElementsFromElement(response, "href").single.text; 53 | findElementsFromElement( 54 | findElementsFromElement(response, "propstat").first, "prop") 55 | .forEach((element) { 56 | final contentLengthElements = 57 | findElementsFromElement(element, "getcontentlength"); 58 | final contentLength = contentLengthElements.isNotEmpty 59 | ? contentLengthElements.single.text 60 | : ""; 61 | 62 | final lastModifiedElements = 63 | findElementsFromElement(element, "getlastmodified"); 64 | final lastModified = lastModifiedElements.isNotEmpty 65 | ? lastModifiedElements.single.text 66 | : ""; 67 | 68 | final creationTimeElements = 69 | findElementsFromElement(element, "creationdate"); 70 | final creationTime = creationTimeElements.isNotEmpty 71 | ? creationTimeElements.single.text 72 | : DateTime.fromMillisecondsSinceEpoch(0).toIso8601String(); 73 | 74 | // Add the just found file to the tree 75 | tree.add(new FileInfo(davItemName, contentLength, 76 | lastModified, DateTime.parse(creationTime), "")); 77 | }); 78 | }); 79 | // Remove root directory 80 | tree.removeAt(0); 81 | // Return the tree 82 | return tree; 83 | } 84 | 85 | List findAllElementsFromDocument( 86 | xml.XmlDocument document, String tag) => 87 | document.findAllElements(tag, namespace: '*').toList(); 88 | 89 | List findElementsFromElement( 90 | xml.XmlElement element, String tag) => 91 | element.findElements(tag, namespace: '*').toList(); 92 | -------------------------------------------------------------------------------- /lib/webdav.dart: -------------------------------------------------------------------------------- 1 | library webdav; 2 | 3 | export 'package:webdav/src/client.dart' show Client; 4 | export 'package:webdav/src/file.dart' show FileInfo; 5 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: webdav 2 | version: 1.0.9 3 | 4 | author: timestee 5 | 6 | description: easy webdav for dart,support basic auth,directories\files listing,upload\downliad. 7 | homepage: https://github.com/timestee/dart-webdav 8 | 9 | environment: 10 | sdk: ">=2.12.0 <3.0.0" 11 | 12 | dependencies: 13 | retry: ^3.1.0 14 | xml: ^5.0.2 15 | 16 | dev_dependencies: 17 | pedantic: ^1.11.0 18 | test: '>=1.0.0 <2.0.0' 19 | -------------------------------------------------------------------------------- /test/webdav_test.dart: -------------------------------------------------------------------------------- 1 | import "package:test/test.dart"; 2 | import 'package:webdav/webdav.dart' as webdav; 3 | 4 | void main() { 5 | webdav.Client _client = 6 | webdav.Client("https://dav.jianguoyun.com/dav", "username", "password"); 7 | 8 | test('ls command', () async { 9 | List list = await _client.ls(); 10 | for (webdav.FileInfo item in list) { 11 | print(item.path); 12 | print( 13 | " - ${item.contentType} | ${item.size}, | ${item.creationTime}, | ${item.modificationTime}"); 14 | } 15 | }); 16 | test('mkdir & mkdirs &cd & rmdir command', () async { 17 | await _client.mkdir("test0"); 18 | _client.cd("test0"); 19 | await _client.mkdirs("test1/test2"); 20 | await _client.rmdir("/test0"); 21 | }); 22 | } 23 | --------------------------------------------------------------------------------