1 /** 2 * Authors: Szabo Bogdan <szabobogdan@yahoo.com> 3 * Date: 2 25, 2015 4 * License: Subject to the terms of the MIT license, as written in the included LICENSE.txt file. 5 * Copyright: Public Domain 6 */ 7 module vibedav.plugins.filedav; 8 9 import vibedav.base; 10 11 import vibe.core.log; 12 import vibe.core.file; 13 import vibe.inet.mimetypes; 14 import vibe.inet.message; 15 import vibe.http.server; 16 import vibe.http.fileserver; 17 import vibe.http.router : URLRouter; 18 import vibe.stream.operations; 19 import vibe.utils.dictionarylist; 20 import vibe.stream.stdio; 21 import vibe.stream.memory; 22 import vibe.utils.memory; 23 24 import std.conv : to; 25 import std.file; 26 import std.path; 27 import std.digest.md; 28 import std.datetime; 29 import std..string; 30 import std.stdio; 31 import std.typecons; 32 import std.uri; 33 import std.uuid; 34 import std.exception; 35 36 static if (__traits(compiles, { import std.algorithm.comparison : max; })) 37 import std.algorithm.comparison : max; 38 else 39 import std.algorithm; 40 41 /// Compute a file etag 42 string eTag(string path) { 43 import std.digest.crc; 44 import std.stdio; 45 46 string fileHash = path; 47 48 if(!path.isDir) { 49 auto f = File(path, "r"); 50 foreach (ubyte[] buffer; f.byChunk(4096)) { 51 ubyte[4] hash = crc32Of(buffer); 52 fileHash ~= crcHexString(hash); 53 } 54 } 55 56 fileHash ~= path.lastModified.toISOExtString ~ path.contentLength.to!string; 57 58 auto etag = hexDigest!MD5(path ~ fileHash); 59 return etag.to!string; 60 } 61 62 SysTime lastModified(string path) { 63 FileInfo dirent = getFileInfo(path); 64 return dirent.timeModified.toUTC; 65 } 66 67 SysTime creationDate(string path) { 68 FileInfo dirent = getFileInfo(path); 69 return dirent.timeCreated.toUTC; 70 } 71 72 ulong contentLength(string path) { 73 FileInfo dirent = getFileInfo(path); 74 return dirent.size; 75 } 76 77 FileStream toStream(string path) { 78 return openFile(path); 79 } 80 81 Path[] getFolderContent(string format = "*")(string path, Path rootPath, Path rootUrl) { 82 Path[] list; 83 rootPath.endsWithSlash = true; 84 string strRootPath = rootPath.toString; 85 86 auto p = Path(path); 87 p.endsWithSlash = true; 88 path = p.toString; 89 90 enforce(path.isDir, "I was expecting to get a dir content"); 91 enforce(strRootPath.length <= path.length); 92 enforce(strRootPath == path[0..strRootPath.length]); 93 94 auto fileList = dirEntries(path, format, SpanMode.shallow); 95 96 foreach(file; fileList) { 97 auto filePath = Path(file[strRootPath.length..$]); 98 filePath.endsWithSlash = false; 99 100 list ~= filePath; 101 } 102 103 return list; 104 } 105 106 class DirectoryResourcePlugin : IDavResourcePlugin { 107 private { 108 Path baseUrlPath; 109 Path basePath; 110 } 111 112 this(Path baseUrlPath, Path basePath) { 113 this.baseUrlPath = baseUrlPath; 114 this.basePath = basePath; 115 } 116 117 Path filePath(URL url) { 118 return getFilePath(baseUrlPath, basePath, url); 119 } 120 121 pure nothrow { 122 bool canSetContent(DavResource resource) { 123 return false; 124 } 125 126 bool canGetStream(DavResource resource) { 127 return false; 128 } 129 130 bool canGetProperty(DavResource resource, string name) { 131 return false; 132 } 133 134 bool canSetProperty(DavResource resource, string name) { 135 return false; 136 } 137 138 bool canRemoveProperty(DavResource resource, string name) { 139 return false; 140 } 141 } 142 143 void setContent(DavResource resource, const ubyte[] content) { 144 throw new DavException(HTTPStatus.internalServerError, "Can't set directory stream."); 145 } 146 147 void setContent(DavResource resource, InputStream content, ulong size) { 148 throw new DavException(HTTPStatus.internalServerError, "Can't set directory stream."); 149 } 150 151 InputStream stream(DavResource resource) { 152 throw new DavException(HTTPStatus.internalServerError, "Can't get directory stream."); 153 } 154 155 void copyPropertiesTo(URL source, URL destination) { 156 157 } 158 159 DavProp property(DavResource resource, string name) { 160 throw new DavException(HTTPStatus.internalServerError, "Can't get property."); 161 } 162 163 HTTPStatus setProperty(DavResource resource, string name, DavProp prop) { 164 throw new DavException(HTTPStatus.internalServerError, "Can't set property."); 165 } 166 167 HTTPStatus removeProperty(DavResource resource, string name) { 168 throw new DavException(HTTPStatus.internalServerError, "Can't remove property."); 169 } 170 171 pure nothrow @property { 172 string name() { 173 return "DirectoryResourcePlugin"; 174 } 175 } 176 } 177 178 class FileResourcePlugin : IDavResourcePlugin { 179 private { 180 Path baseUrlPath; 181 Path basePath; 182 } 183 184 this(Path baseUrlPath, Path basePath) { 185 this.baseUrlPath = baseUrlPath; 186 this.basePath = basePath; 187 } 188 189 Path filePath(URL url) { 190 return getFilePath(baseUrlPath, basePath, url); 191 } 192 193 bool canSetContent(DavResource resource) { 194 auto filePath = filePath(resource.url).toString; 195 return filePath.exists; 196 } 197 198 bool canGetStream(DavResource resource) { 199 return canSetContent(resource); 200 } 201 202 bool canGetProperty(DavResource resource, string name) { 203 return false; 204 } 205 206 bool canSetProperty(DavResource resource, string name) { 207 return false; 208 } 209 210 bool canRemoveProperty(DavResource resource, string name) { 211 return false; 212 } 213 214 void setContent(DavResource resource, const ubyte[] content) { 215 auto filePath = filePath(resource.url).toString; 216 std.stdio.write(filePath, content); 217 } 218 219 void setContent(DavResource resource, InputStream content, ulong size) { 220 auto nativePath = filePath(resource.url).toString; 221 222 auto tmpPath = nativePath ~ ".tmp"; 223 auto tmpFile = File(tmpPath, "w"); 224 225 while(!content.empty) { 226 auto leastSize = content.leastSize; 227 ubyte[] buf; 228 buf.length = leastSize; 229 content.read(buf); 230 tmpFile.rawWrite(buf); 231 } 232 233 tmpFile.flush; 234 235 std.file.copy(tmpPath, nativePath); 236 std.file.remove(tmpPath); 237 } 238 239 InputStream stream(DavResource resource) { 240 auto nativePath = filePath(resource.url).toString; 241 return nativePath.toStream; 242 } 243 244 DavProp property(DavResource resource, string name) { 245 throw new DavException(HTTPStatus.internalServerError, "Can't get property."); 246 } 247 248 void copyPropertiesTo(URL source, URL destination) { 249 250 } 251 252 HTTPStatus setProperty(DavResource resource, string name, DavProp prop) { 253 throw new DavException(HTTPStatus.internalServerError, "Can't set property."); 254 } 255 256 HTTPStatus removeProperty(DavResource resource, string name) { 257 throw new DavException(HTTPStatus.internalServerError, "Can't remove property."); 258 } 259 260 pure nothrow @property { 261 string name() { 262 return "FileResourcePlugin"; 263 } 264 } 265 } 266 267 /// File Dav impplementation 268 class FileDav : BaseDavPlugin { 269 270 protected { 271 Path baseUrlPath; 272 Path basePath; 273 } 274 275 this(IDav dav, Path baseUrlPath, Path basePath) { 276 super(dav); 277 this.baseUrlPath = baseUrlPath; 278 this.basePath = basePath; 279 } 280 281 protected { 282 void setResourceProperties(DavResource resource) { 283 string path = filePath(resource.url).toString; 284 assert(path.exists, "Can't set basic properties. The path does not exist."); 285 286 setResourceInfoProperties(resource); 287 288 if(path.isDir) 289 setCollection(resource); 290 } 291 292 void setCollection(DavResource resource) { 293 resource.resourceType ~= "collection:DAV:"; 294 } 295 296 void setResourceInfoProperties(DavResource resource) { 297 string path = filePath(resource.url).toString; 298 299 assert(path.exists); 300 301 resource.creationDate = creationDate(path); 302 resource.lastModified = lastModified(path); 303 resource.eTag = eTag(path); 304 resource.contentType = getMimeTypeForFile(path); 305 resource.contentLength = contentLength(path); 306 resource.name = baseName(path); 307 } 308 } 309 310 Path filePath(URL url) { 311 return getFilePath(baseUrlPath, basePath, url); 312 } 313 314 override { 315 bool exists(URL url, string username) { 316 auto filePath = filePath(url); 317 318 return filePath.toString.exists; 319 } 320 321 322 Path[] childList(URL url, string username) { 323 Path[] list; 324 325 auto nativePath = filePath(url).toString; 326 327 if(nativePath.exists) 328 list = getFolderContent!"*"(nativePath, basePath, baseUrlPath); 329 330 return list; 331 } 332 333 334 bool canCreateResource(URL url, string username) { 335 return !exists(url, username); 336 } 337 338 bool canCreateCollection(URL url, string username) { 339 return !exists(url, username); 340 } 341 342 void removeResource(URL url, string username) { 343 if(!exists(url, username)) 344 throw new DavException(HTTPStatus.notFound, "`" ~ url.toString ~ "` not found."); 345 346 auto filePath = filePath(url).toString; 347 348 if(filePath.isDir) 349 filePath.rmdirRecurse; 350 else 351 filePath.remove; 352 } 353 354 DavResource getResource(URL url, string username) { 355 if(!exists(url, username)) 356 throw new DavException(HTTPStatus.notFound, "`" ~ url.toString ~ "` not found."); 357 358 auto filePath = filePath(url); 359 360 DavResource resource = new DavResource(_dav, url); 361 resource.username = username; 362 setResourceProperties(resource); 363 364 return resource; 365 } 366 367 DavResource createResource(URL url, string username) { 368 auto filePath = filePath(url).toString; 369 370 File(filePath, "w"); 371 372 return getResource(url, username); 373 } 374 375 DavResource createCollection(URL url, string username) { 376 auto filePath = filePath(url); 377 378 if(filePath.toString.exists) 379 throw new DavException(HTTPStatus.methodNotAllowed, "Resource already exists."); 380 381 filePath.toString.mkdirRecurse; 382 383 return getResource(url, username); 384 } 385 386 void bindResourcePlugins(DavResource resource) { 387 if(resource.isCollection) 388 resource.registerPlugin(new DirectoryResourcePlugin(baseUrlPath, basePath)); 389 else 390 resource.registerPlugin(new FileResourcePlugin(baseUrlPath, basePath)); 391 392 resource.registerPlugin(new ResourceCustomProperties); 393 resource.registerPlugin(new ResourceBasicProperties); 394 } 395 396 @property { 397 IDav dav() { 398 return _dav; 399 } 400 401 string[] support(URL url, string username) { 402 return ["1", "2", "3"]; 403 } 404 } 405 } 406 407 @property { 408 string name() { 409 return "FileDav"; 410 } 411 } 412 } 413 414 IDav serveFileDav(URLRouter router, string rootUrl, string rootPath) { 415 rootUrl = rootUrl.stripSlashes; 416 rootPath = rootPath.stripSlashes; 417 418 auto dav = new Dav(rootUrl); 419 auto fileDav = new FileDav(dav, Path(rootUrl), Path(rootPath)); 420 421 if(rootUrl != "") rootUrl = "/"~rootUrl~"/"; 422 423 router.any(rootUrl ~ "*", serveDav(dav)); 424 425 return dav; 426 }