├── library.properties ├── library.json ├── examples └── simple │ └── simple.ino ├── README.md └── src ├── AsyncWebdav.h └── AsyncWebdav.cpp /library.properties: -------------------------------------------------------------------------------- 1 | name=ESP Async Webdav 2 | version=1.0.0 3 | author=Rostwolke 4 | maintainer=Rostwolke 5 | sentence=Async Webdav Plugin for ESPAsyncWebServer 6 | paragraph=Async Webdav Plugin for ESPAsyncWebServer 7 | category=Other 8 | url=https://github.com/rostwolke/ESPAsyncWebdav 9 | architectures=* 10 | -------------------------------------------------------------------------------- /library.json: -------------------------------------------------------------------------------- 1 | { 2 | "name":"ESPAsyncWebdav", 3 | "description":"Asynchronous Webdav Plugin for ESPAsyncWebServer", 4 | "keywords":"async,tcp,server,webdav", 5 | "authors": 6 | { 7 | "name": "Dennis Parsch", 8 | "maintainer": true 9 | }, 10 | "repository": 11 | { 12 | "type": "git", 13 | "url": "https://github.com/rostwolke/ESPAsyncWebdav" 14 | }, 15 | "frameworks": "arduino", 16 | "platforms":"espressif" 17 | } 18 | -------------------------------------------------------------------------------- /examples/simple/simple.ino: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | 7 | const char* ssid = "ssid"; 8 | const char* password = "pass"; 9 | 10 | AsyncWebServer server(80); 11 | AsyncWebdav dav("/dav"); 12 | 13 | 14 | void setup(void){ 15 | // init serial and wifi 16 | Serial.begin(115200); 17 | WiFi.begin(ssid, password); 18 | Serial.println(""); 19 | 20 | // wait for connection 21 | while (WiFi.status() != WL_CONNECTED){ 22 | delay(500); 23 | Serial.print("."); 24 | } 25 | Serial.println(""); 26 | Serial.print("Connected to "); 27 | Serial.println(ssid); 28 | Serial.print("IP address: "); 29 | Serial.println(WiFi.localIP()); 30 | 31 | // init spiffs 32 | LittleFS.begin(); 33 | 34 | // add websocket handler 35 | server.addHandler(&dav); 36 | 37 | // start webserver 38 | server.serveStatic("/", LittleFS, "/").setDefaultFile("index.html"); 39 | server.begin(); 40 | } 41 | 42 | 43 | void loop(void){ 44 | } 45 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ESPAsyncWebdav 2 | This project is a Webdav Plugin for ESPAsyncWebServer. So far it has only been tested with an ESP8266 but should 3 | also work for the ESP32. 4 | 5 | ## Important notice 6 | 7 | To make this plugin work with ESPAsyncWebServer you currently (as of 2020-01-22) have to either patch me-no-dev's original code or use [my fork](https://github.com/rostwolke/ESPAsyncWebServer). To see which changes have to be done, see the [pull request](https://github.com/me-no-dev/ESPAsyncWebServer/pull/676). 8 | 9 | ## How to use 10 | - Download the project archive and unpack it in your Arduino libraries folder 11 | - Ensure that the folder name is ESPAsyncWebdav (remove the -master) 12 | - Include the library in your project together with LittleFS 13 | ``` 14 | #include 15 | #include 16 | ``` 17 | - Initialize the plugin in a global variable (the parameter is the prefix path to access the webdav server) 18 | ``` 19 | AsyncWebdav dav("/dav"); 20 | ``` 21 | - Initialize the file system in setup() 22 | ``` 23 | LittleFS.begin(); 24 | ``` 25 | - Add the webdav handler to the webserver 26 | ``` 27 | server.addHandler(&dav); 28 | ``` 29 | That's it! To see the a full example, you can check the examples directory in the project. 30 | 31 | ## Access/Mount the ESP 32 | To access the drive from Windows, open \\esp_hostname_or_ip\path_in_constructor\DavWWWRoot in the Windows Explorer, 33 | or use the Map Network Drive menu. The software works with NetDrive and WinSCP. 34 | -------------------------------------------------------------------------------- /src/AsyncWebdav.h: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | 4 | enum DavResourceType { DAV_RESOURCE_NONE, DAV_RESOURCE_FILE, DAV_RESOURCE_DIR }; 5 | enum DavDepthType { DAV_DEPTH_NONE, DAV_DEPTH_CHILD, DAV_DEPTH_ALL }; 6 | 7 | 8 | class AsyncWebdav: public AsyncWebHandler { 9 | public: 10 | AsyncWebdav(const String& url); 11 | 12 | virtual bool canHandle(AsyncWebServerRequest *request) override final; 13 | virtual void handleRequest(AsyncWebServerRequest *request) override final; 14 | virtual void handleBody(AsyncWebServerRequest *request, unsigned char *data, size_t len, size_t index, size_t total) override final; 15 | const char* url() const { 16 | return _url.c_str(); 17 | } 18 | 19 | private: 20 | String _url; 21 | 22 | void handlePropfind(const String& path, DavResourceType resource, AsyncWebServerRequest * request); 23 | void handleGet(const String& path, DavResourceType resource, AsyncWebServerRequest * request); 24 | void handlePut(const String& path, DavResourceType resource, AsyncWebServerRequest * request, unsigned char *data, size_t len, size_t index, size_t total); 25 | void handleLock(const String& path, DavResourceType resource, AsyncWebServerRequest * request); 26 | void handleUnlock(const String& path, DavResourceType resource, AsyncWebServerRequest * request); 27 | void handleMkcol(const String& path, DavResourceType resource, AsyncWebServerRequest * request); 28 | void handleMove(const String& path, DavResourceType resource, AsyncWebServerRequest * request); 29 | void handleDelete(const String& path, DavResourceType resource, AsyncWebServerRequest * request); 30 | void handleHead(DavResourceType resource, AsyncWebServerRequest * request); 31 | void handleNotFound(AsyncWebServerRequest * request); 32 | void sendPropResponse(AsyncResponseStream *response, boolean recursing, File *curFile); 33 | String urlToUri(String url); 34 | 35 | }; -------------------------------------------------------------------------------- /src/AsyncWebdav.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include "AsyncWebdav.h" 8 | 9 | AsyncWebdav::AsyncWebdav(const String& url){ 10 | this->_url = url; 11 | } 12 | 13 | 14 | bool AsyncWebdav::canHandle(AsyncWebServerRequest *request){ 15 | if(request->url().startsWith(this->_url)){ 16 | if(request->method() == HTTP_PROPFIND 17 | || request->method() == HTTP_PROPPATCH){ 18 | request->addInterestingHeader("depth"); 19 | return true; 20 | } 21 | if(request->method() == HTTP_MOVE){ 22 | request->addInterestingHeader("destination"); 23 | return true; 24 | } 25 | if(request->method() == HTTP_GET 26 | || request->method() == HTTP_HEAD 27 | || request->method() == HTTP_OPTIONS 28 | || request->method() == HTTP_PUT 29 | || request->method() == HTTP_LOCK 30 | || request->method() == HTTP_UNLOCK 31 | || request->method() == HTTP_MKCOL 32 | || request->method() == HTTP_DELETE 33 | || request->method() == HTTP_COPY){ 34 | return true; 35 | } 36 | } 37 | return false; 38 | } 39 | 40 | void AsyncWebdav::handleRequest(AsyncWebServerRequest *request){ 41 | // parse the url to a proper path 42 | String path = request->url().substring(this->_url.length()); 43 | if(path.isEmpty()){ 44 | path = "/"; 45 | } 46 | if(!path.equals("/") && path.endsWith("/")){ 47 | path = path.substring(0, path.length() - 1); 48 | } 49 | 50 | // check resource type on local storage 51 | DavResourceType resource = DAV_RESOURCE_NONE; 52 | File baseFile = LittleFS.open(path, "r"); 53 | if(baseFile){ 54 | resource = baseFile.isDirectory() ? DAV_RESOURCE_DIR : DAV_RESOURCE_FILE; 55 | baseFile.close(); 56 | } 57 | 58 | // route the request 59 | if(request->method() == HTTP_PROPFIND || request->method() == HTTP_PROPPATCH){ 60 | return handlePropfind(path, resource, request); 61 | } 62 | if(request->method() == HTTP_GET){ 63 | return handleGet(path, resource, request); 64 | } 65 | if(request->method() == HTTP_HEAD || request->method() == HTTP_OPTIONS){ 66 | return handleHead(resource, request); 67 | } 68 | if(request->method() == HTTP_PUT){ 69 | if(LittleFS.exists(path)){ 70 | return request->send(200); 71 | }else{ 72 | File f = LittleFS.open(path, "a"); 73 | if(f){ 74 | f.close(); 75 | return request->send(201); 76 | }else{ 77 | return request->send(500); 78 | } 79 | } 80 | } 81 | if(request->method() == HTTP_LOCK){ 82 | return handleLock(path, resource, request); 83 | } 84 | if(request->method() == HTTP_UNLOCK){ 85 | return handleUnlock(path, resource, request); 86 | } 87 | if(request->method() == HTTP_MKCOL){ 88 | return handleMkcol(path, resource, request); 89 | } 90 | if(request->method() == HTTP_MOVE){ 91 | return handleMove(path, resource, request); 92 | } 93 | if(request->method() == HTTP_DELETE){ 94 | return handleDelete(path, resource, request); 95 | } 96 | 97 | return handleNotFound(request); 98 | } 99 | 100 | void AsyncWebdav::handleBody(AsyncWebServerRequest *request, unsigned char *data, size_t len, size_t index, size_t total){ 101 | // parse the url to a proper path 102 | String path = request->url().substring(this->_url.length()); 103 | if(path.isEmpty()){ 104 | path = "/"; 105 | } 106 | if(!path.equals("/") && path.endsWith("/")){ 107 | path = path.substring(0, path.length() - 1); 108 | } 109 | 110 | // check resource type on local storage 111 | DavResourceType resource = DAV_RESOURCE_NONE; 112 | File baseFile = LittleFS.open(path, "r"); 113 | if(baseFile){ 114 | resource = baseFile.isDirectory() ? DAV_RESOURCE_DIR : DAV_RESOURCE_FILE; 115 | baseFile.close(); 116 | } 117 | 118 | // route the request 119 | if(request->method() == HTTP_PUT){ 120 | return handlePut(path, resource, request, data, len, index, total); 121 | } 122 | } 123 | 124 | void AsyncWebdav::handlePropfind(const String& path, DavResourceType resource, AsyncWebServerRequest * request){ 125 | // check whether file or dir exists 126 | if(resource == DAV_RESOURCE_NONE){ 127 | return handleNotFound(request); 128 | } 129 | 130 | // check depth header 131 | DavDepthType depth = DAV_DEPTH_NONE; 132 | AsyncWebHeader* depthHeader = request->getHeader("Depth"); 133 | if(depthHeader){ 134 | if(depthHeader->value().equals("1")){ 135 | depth = DAV_DEPTH_CHILD; 136 | } else if(depthHeader->value().equals("infinity")){ 137 | depth = DAV_DEPTH_ALL; 138 | } 139 | } 140 | 141 | // prepare response 142 | File baseFile = LittleFS.open(path, "r"); 143 | AsyncResponseStream *response = request->beginResponseStream("application/xml"); 144 | response->setCode(207); 145 | 146 | response->print(""); 147 | response->print(""); 148 | sendPropResponse(response, false, &baseFile); 149 | if(resource == DAV_RESOURCE_DIR && depth == DAV_DEPTH_CHILD){ 150 | Dir dir = LittleFS.openDir(path); 151 | File childFile; 152 | while(dir.next()){ 153 | childFile = dir.openFile("r"); 154 | sendPropResponse(response, true, &childFile); 155 | childFile.close(); 156 | } 157 | } 158 | response->print(""); 159 | 160 | baseFile.close(); 161 | return request->send(response); 162 | } 163 | 164 | void AsyncWebdav::handleGet(const String& path, DavResourceType resource, AsyncWebServerRequest * request){ 165 | if(resource != DAV_RESOURCE_FILE){ 166 | return handleNotFound(request); 167 | } 168 | 169 | AsyncWebServerResponse *response = request->beginResponse(LittleFS, path); 170 | response->addHeader("Allow", "PROPFIND,OPTIONS,DELETE,COPY,MOVE,HEAD,POST,PUT,GET"); 171 | request->send(response); 172 | } 173 | 174 | void AsyncWebdav::handlePut(const String& path, DavResourceType resource, AsyncWebServerRequest * request, unsigned char *data, size_t len, size_t index, size_t total){ 175 | if(resource == DAV_RESOURCE_DIR){ 176 | return handleNotFound(request); 177 | } 178 | 179 | File file; 180 | if(!index){ 181 | file = LittleFS.open(path, "w"); 182 | }else{ 183 | file = LittleFS.open(path, "a"); 184 | } 185 | file.write(data, len); 186 | file.close(); 187 | } 188 | 189 | void AsyncWebdav::handleLock(const String& path, DavResourceType resource, AsyncWebServerRequest * request){ 190 | if(resource == DAV_RESOURCE_NONE){ 191 | return handleNotFound(request); 192 | } 193 | 194 | AsyncResponseStream *response = request->beginResponseStream("application/xml; charset=utf-8"); 195 | response->setCode(200); 196 | response->addHeader("Allow", "PROPPATCH,PROPFIND,OPTIONS,DELETE,UNLOCK,COPY,LOCK,MOVE,HEAD,POST,PUT,GET"); 197 | response->addHeader("Lock-Token", "urn:uuid:26e57cb3-834d-191a-00de-000042bdecf9"); 198 | 199 | response->print(""); 200 | response->print(""); 201 | response->print(""); 202 | response->print(""); 203 | response->print(""); 204 | response->print(""); 205 | response->print("urn:uuid:26e57cb3-834d-191a-00de-000042bdecf9"); 206 | response->printf("%s", (String("http://") + request->host() + "/dav" + path).c_str()); 207 | response->print("infinity"); 208 | response->printf("%s", "todo"); 209 | response->print("Second-3600"); 210 | response->print(""); 211 | response->print(""); 212 | response->print(""); 213 | 214 | request->send(response); 215 | } 216 | 217 | void AsyncWebdav::handleUnlock(const String& path, DavResourceType resource, AsyncWebServerRequest * request){ 218 | if(resource == DAV_RESOURCE_NONE){ 219 | return handleNotFound(request); 220 | } 221 | 222 | AsyncWebServerResponse *response = request->beginResponse(200); 223 | response->addHeader("Allow", "PROPPATCH,PROPFIND,OPTIONS,DELETE,UNLOCK,COPY,LOCK,MOVE,HEAD,POST,PUT,GET"); 224 | response->addHeader("Lock-Token", "urn:uuid:26e57cb3-834d-191a-00de-000042bdecf9"); 225 | request->send(response); 226 | } 227 | 228 | void AsyncWebdav::handleMkcol(const String& path, DavResourceType resource, AsyncWebServerRequest * request){ 229 | // does the file/dir already exist? 230 | int status; 231 | if(resource != DAV_RESOURCE_NONE){ 232 | status = 405; 233 | }else{ 234 | // create dir and send response 235 | if(LittleFS.mkdir(path)){ 236 | status = 201; 237 | }else{ 238 | status = 405; 239 | } 240 | } 241 | request->send(status); 242 | } 243 | 244 | void AsyncWebdav::handleMove(const String& path, DavResourceType resource, AsyncWebServerRequest * request){ 245 | if(resource == DAV_RESOURCE_NONE){ 246 | return handleNotFound(request); 247 | } 248 | 249 | AsyncWebHeader* destinationHeader = request->getHeader("destination"); 250 | if(!destinationHeader || destinationHeader->value().isEmpty()){ 251 | return handleNotFound(request); 252 | } 253 | 254 | AsyncWebServerResponse *response; 255 | if(LittleFS.rename(path, urlToUri(destinationHeader->value()))){ 256 | response = request->beginResponse(201); 257 | response->addHeader("Allow", "OPTIONS,MKCOL,LOCK,POST,PUT"); 258 | }else{ 259 | response = request->beginResponse(500, "text/plain", "Unable to move"); 260 | } 261 | 262 | request->send(response); 263 | } 264 | 265 | void AsyncWebdav::handleDelete(const String& path, DavResourceType resource, AsyncWebServerRequest * request){ 266 | // does the file/dir exist? 267 | if(resource == DAV_RESOURCE_NONE){ 268 | return handleNotFound(request); 269 | } 270 | 271 | // delete file or dir 272 | bool result; 273 | if(resource == DAV_RESOURCE_FILE){ 274 | result = LittleFS.remove(path); 275 | }else{ 276 | result = LittleFS.rmdir(path); 277 | } 278 | 279 | // check for error 280 | AsyncWebServerResponse *response; 281 | if(result) { 282 | response = request->beginResponse(200); 283 | response->addHeader("Allow", "OPTIONS,MKCOL,LOCK,POST,PUT"); 284 | }else{ 285 | response = request->beginResponse(201); 286 | } 287 | 288 | request->send(response); 289 | } 290 | 291 | void AsyncWebdav::handleHead(DavResourceType resource, AsyncWebServerRequest * request){ 292 | if(resource == DAV_RESOURCE_NONE){ 293 | return handleNotFound(request); 294 | } 295 | 296 | AsyncWebServerResponse *response = request->beginResponse(200); 297 | if(resource == DAV_RESOURCE_FILE){ 298 | response->addHeader("Allow", "PROPFIND,OPTIONS,DELETE,COPY,MOVE,HEAD,POST,PUT,GET"); 299 | } 300 | if(resource == DAV_RESOURCE_DIR){ 301 | response->addHeader("Allow", "PROPFIND,OPTIONS,DELETE,COPY,MOVE"); 302 | } 303 | request->send(response); 304 | } 305 | 306 | void AsyncWebdav::handleNotFound(AsyncWebServerRequest * request){ 307 | AsyncWebServerResponse *response = request->beginResponse(404); 308 | response->addHeader("Allow", "OPTIONS,MKCOL,POST,PUT"); 309 | request->send(response); 310 | } 311 | 312 | String AsyncWebdav::urlToUri(String url){ 313 | if(url.startsWith("http://")){ 314 | int uriStart = url.indexOf('/', 7); 315 | return url.substring(uriStart + 4); 316 | }else if(url.startsWith("https://")){ 317 | int uriStart = url.indexOf('/', 8); 318 | return url.substring(uriStart + 4); 319 | }else{ 320 | return url.substring(this->_url.length()); 321 | } 322 | } 323 | 324 | void AsyncWebdav::sendPropResponse(AsyncResponseStream *response, boolean recursing, File *curFile){ 325 | String fullPath = curFile->fullName(); 326 | if(fullPath.substring(0, 1) != "/"){ 327 | fullPath = String("/") + fullPath; 328 | } 329 | if(curFile->isDirectory() && fullPath.substring(fullPath.length() - 1, fullPath.length()) != "/"){ 330 | fullPath += "/"; 331 | } 332 | fullPath = this->_url + fullPath; 333 | fullPath.replace(" ", "%20"); 334 | 335 | // get file modified time 336 | time_t lastWrite = curFile->getLastWrite(); 337 | DateTimeClass dt(lastWrite); 338 | String fileTimeStamp = dt.format("%a, %d %b %Y %H:%M:%S GMT"); 339 | 340 | // load fs info 341 | FSInfo fsInfo; 342 | LittleFS.info(fsInfo); 343 | 344 | // send response 345 | response->print(""); 346 | response->printf("%s", fullPath.c_str()); 347 | response->print(""); 348 | response->print(""); 349 | 350 | // last modified 351 | response->printf("%s", fileTimeStamp.c_str()); 352 | 353 | // quota 354 | response->printf("%d", fsInfo.usedBytes); 355 | response->printf("%d", fsInfo.totalBytes - fsInfo.usedBytes); 356 | 357 | if(curFile->isDirectory()) { 358 | // resource type 359 | response->print(""); 360 | } else { 361 | // etag 362 | response->printf("%s", sha1(fullPath + fileTimeStamp).c_str()); 363 | 364 | // resource type 365 | response->print(""); 366 | 367 | // content length 368 | response->printf("%d", curFile->size()); 369 | 370 | // content type 371 | response->print("text/plain"); 372 | } 373 | response->print(""); 374 | response->print("HTTP/1.1 200 OK"); 375 | response->print(""); 376 | 377 | response->print(""); 378 | } 379 | --------------------------------------------------------------------------------