1 /* 2 * Copyright (c) 2008-2015 LabKey Corporation 3 * 4 * Licensed under the Apache License, Version 2.0: http://www.apache.org/licenses/LICENSE-2.0 5 */ 6 7 /** 8 * A static class for interacting with files and filesystems 9 * @name LABKEY.FileSystem 10 * @ignore 11 * @class 12 */ 13 Ext.ns('LABKEY.FileSystem'); 14 15 16 /** 17 * A helper for manipulating URIs. This is not part of the public API, so do not rely on its existence 18 * from parseUri 1.2.1 19 * (c) 2007 Steven Levithan <stevenlevithan.com> 20 * MIT License 21 * @ignore 22 */ 23 LABKEY.URI = Ext.extend(Object, 24 { 25 constructor : function(u) 26 { 27 this.toString = function() 28 { 29 return this.protocol + "://" + this.host + this.pathname + this.search; 30 }; 31 if (typeof u == "string") 32 this.parse(u); 33 else if (typeof u == "object") 34 Ext.apply(this,u); 35 36 this.options = Ext.apply({},this.options); // clone 37 }, 38 parse: function(str) 39 { 40 var o = this.options; 41 var m = o.parser[o.strictMode ? "strict" : "loose"].exec(str); 42 var uri = this || {}; 43 var i = 14; 44 45 while (i--) 46 uri[o.key[i]] = m[i] || ""; 47 48 if (!uri.protocol) 49 { 50 var l = window.location; 51 uri.protocol = uri.protocol || l.protocol; 52 uri.port = uri.port || l.port; 53 uri.hostname = uri.hostname || l.hostname; 54 uri.host = uri.host || l.host; 55 } 56 if (uri.protocol && uri.protocol.charAt(uri.protocol.length-1) == ":") 57 uri.protocol = uri.protocol.substr(0,uri.protocol.length - 1); 58 59 uri[o.q.name] = {}; 60 uri[o.key[12]].replace(o.q.parser, function ($0, $1, $2) 61 { 62 if ($1) uri[o.q.name][$1] = $2; 63 }); 64 uri.href = this.protocol + "://" + this.host + this.pathname + this.search; 65 return uri; 66 }, 67 options: 68 { 69 strictMode: false, 70 key: ["source","protocol","host","userInfo","user","password","hostname","port","relative","pathname","directory","file","search","hash"], 71 q: 72 { 73 name: "query", 74 parser: /(?:^|&)([^&=]*)=?([^&]*)/g 75 }, 76 parser: 77 { 78 strict: /^(?:([^:\/?#]+):)?(?:\/\/((?:(([^:@]*):?([^:@]*))?@)?([^:\/?#]*)(?::(\d*))?))?((((?:[^?#\/]*\/)*)([^?#]*))(?:\?([^#]*))?(?:#(.*))?)/, 79 loose: /^(?:(?![^:@]+:[^:@\/]*@)([^:\/?#.]+):)?(?:\/\/)?((?:(([^:@]*):?([^:@]*))?@)?([^:\/?#]*)(?::(\d*))?)(((\/(?:[^?#](?![^?#\/]*\.[^?#\/.]+(?:[?#]|$)))*\/?)?([^?#\/]*))(?:\?([^#]*))?(?:#(.*))?)/ 80 } 81 } 82 }); 83 84 /** 85 * Static map of events used internally by LABKEY.FileSystem 86 * @memberOf LABKEY.FileSystem# 87 * @ignore 88 * @private 89 */ 90 LABKEY.FileSystem.FILESYSTEM_EVENTS = { 91 ready: "ready", 92 filesremoved: "filesremoved", 93 fileschanged: "fileschanged" 94 }; 95 96 97 /** 98 * Returns a listing of all file roots available for a given folder 99 * @memberOf LABKEY.FileSystem# 100 * @param config Configuration properties. 101 * @param {String} config.containerPath The container to test. If null, the current container will be used. 102 * @param {Function} config.success Success callback function. It will be called with the following arguments: 103 * <li>Results: A map linking the file root name to the WebDAV URL</li> 104 * <li>Response: The XMLHttpRequest object containing the response data.</li> 105 * @param {Function} [config.failure] Error callback function. It will be called with the following arguments: 106 * <li>Response: The XMLHttpRequest object containing the response data.</li> 107 * <li>Options: The parameter to the request call.</li> 108 * @param {Object} [config.scope] The scope for the callback function. Defaults to 'this' 109 */ 110 LABKEY.FileSystem.getFileRoots = function(config){ 111 config = config || {}; 112 113 LABKEY.Ajax.request({ 114 url: LABKEY.ActionURL.buildURL('filecontent', 'getFileRoots', config.containerPath), 115 scope: config.scope || this, 116 success: LABKEY.Utils.getCallbackWrapper(config.success, config.scope), 117 failure: LABKEY.Utils.getCallbackWrapper(config.failure, config.scope), 118 }); 119 } 120 121 122 /** 123 * Static map of events used internally by LABKEY.FileSystem 124 * @memberOf LABKEY.FileSystem# 125 * @ignore 126 * @private 127 */ 128 LABKEY.FileSystem.BROWSER_EVENTS = { 129 selectionchange:"selectionchange", 130 directorychange:"directorychange", 131 doubleclick:"doubleclick", 132 transferstarted:'transferstarted', 133 transfercomplete:'transfercomplete', 134 movestarted:'movestarted', 135 movecomplete:'movecomplete', 136 deletestarted:'deletestarted', 137 deletecomplete:'deletecomplete' 138 } 139 140 LABKEY.FileSystem.FOLDER_ICON = LABKEY.contextPath + "/" + LABKEY.extJsRoot + "/resources/images/default/tree/folder.gif"; 141 142 143 /** 144 * Private heper methods used internally by LABKEY.Filesystem. 145 * @ignore 146 * @private 147 */ 148 LABKEY.FileSystem.Util = new function(){ 149 var $ = Ext.get, $h = Ext.util.Format.htmlEncode, $dom = Ext.DomHelper, 150 imgSeed = 0, 151 FATAL_INT = 50000, 152 ERROR_INT = 40000, 153 WARN_INT = 30000, 154 INFO_INT = 20000, 155 DEBUG_INT = 10000; 156 157 function formatWithCommas(value) 158 { 159 var x = value; 160 var formatted = (x == 0) ? '0' : ''; 161 var sep = ''; 162 while (x > 0) 163 { 164 // Comma separate between thousands 165 formatted = sep + formatted; 166 formatted = (x % 10) + formatted; 167 x -= (x % 10); 168 if (x > 0) 169 { 170 formatted = ((x % 100) / 10) + formatted; 171 x -= (x % 100); 172 } 173 if (x > 0) 174 { 175 formatted = ((x % 1000) / 100) + formatted; 176 x -= (x % 1000); 177 } 178 x = x / 1000; 179 sep = ','; 180 } 181 return formatted; 182 } 183 184 return { 185 startsWith : function(s, f){ 186 var len = f.length; 187 if (s.length < len) return false; 188 if (len == 0) 189 return true; 190 return s.charAt(0) == f.charAt(0) && s.charAt(len-1) == f.charAt(len-1) && s.indexOf(f) == 0; 191 }, 192 193 endsWith : function(s, f){ 194 var len = f.length; 195 var slen = s.length; 196 if (slen < len) return false; 197 if (len == 0) 198 return true; 199 return s.charAt(slen-len) == f.charAt(0) && s.charAt(slen-1) == f.charAt(len-1) && s.indexOf(f) == slen-len; 200 }, 201 202 // minor hack call with scope having decorateIcon functions 203 renderIcon : function(value, metadata, record, rowIndex, colIndex, store, decorateFN) { 204 var file = record.get("file"); 205 if (!value) 206 { 207 if (!file) 208 { 209 value = LABKEY.FileSystem.FOLDER_ICON; 210 } 211 else 212 { 213 var name = record.get("name"); 214 var i = name.lastIndexOf("."); 215 var ext = i >= 0 ? name.substring(i) : name; 216 value = LABKEY.contextPath + "/project/icon.view?name=" + ext; 217 } 218 } 219 var img = {tag:'img', width:16, height:16, src:value, id:'img'+(++imgSeed)}; 220 if (decorateFN) 221 decorateFN.defer(1,this,[img.id,record]); 222 return $dom.markup(img); 223 }, 224 225 renderFileSize : function(value, metadata, record, rowIndex, colIndex, store){ 226 if (!record.get('file')) return ""; 227 var f = Ext.util.Format.fileSize(value); 228 return "<span title='" + f + "'>" + formatWithCommas(value) + "</span>"; 229 }, 230 231 /* Used as a field renderer */ 232 renderUsage : function(value, metadata, record, rowIndex, colIndex, store){ 233 if (!value || value.length == 0) return ""; 234 var result = "<span title='"; 235 for (var i = 0; i < value.length; i++) 236 { 237 if (i > 0) 238 { 239 result = result + ", "; 240 } 241 result = result + $h(value[i].message); 242 } 243 result = result + "'>"; 244 for (i = 0; i < value.length; i++) 245 { 246 if (i > 0) 247 { 248 result = result + ", "; 249 } 250 if (value[i].href) 251 { 252 result = result + "<a href=\'" + $h(value[i].href) + "'>"; 253 } 254 result = result + $h(value[i].message); 255 if (value[i].href) 256 { 257 result = result + "</a>"; 258 } 259 } 260 result = result + "</span>"; 261 return result; 262 }, 263 264 renderDateTime : function(value, metadata, record, rowIndex, colIndex, store){ 265 if (!value) return ""; 266 if (value.getTime() == 0) return ""; 267 return "<span title='" + LABKEY.FileSystem.Util._longDateTime(value) + "'>" + LABKEY.FileSystem.Util. _rDateTime(value) + "<span>"; 268 }, 269 270 _longDateTime : Ext.util.Format.dateRenderer("l, F d, Y g:i:s A"), 271 _rDateTime : Ext.util.Format.dateRenderer("Y-m-d H:i:s"), 272 273 formatWithCommas : function(value) { 274 return formatWithCommas(value); 275 }, 276 277 _processAjaxResponse: function(response){ 278 if (response && 279 response.responseText && 280 response.getResponseHeader('Content-Type') && 281 response.getResponseHeader('Content-Type').indexOf('application/json') >= 0) 282 { 283 try 284 { 285 response.jsonResponse = Ext.util.JSON.decode(response.responseText); 286 if(response.jsonResponse.status) 287 response.status = response.jsonResponse.status 288 } 289 catch (error){ 290 //ignore 291 } 292 } 293 } 294 } 295 } 296 297 /** 298 * This is a base class that is extended by LABKEY.FileSystem.WebdavFileSystem and others. It is not intended to be used directly. 299 * @class LABKEY.FileSystem.AbstractFileSystem 300 * @name LABKEY.FileSystem.AbstractFileSystem 301 * @param config Configuration properties. 302 */ 303 304 /** 305 * The Ext.Record type used to store files in the fileSystem 306 * @name FileRecord 307 * @fieldOf LABKEY.FileSystem.AbstractFileSystem# 308 * @description 309 * The file record should contain the following fields: 310 <li>uri (string, urlencoded)</li> 311 <li>path (string, not encoded)</li> 312 <li>name (string)</li> 313 <li>file (bool)</li> 314 <li>created (date)</li> 315 <li>modified (date)</li> 316 <li>size (int)</li> 317 <li>createdBy(string, optional)</li> 318 <li>modifiedBy(string, optional)</li> 319 <li>iconHref(string, optional)</li> 320 <li>actionHref(string, optional)</li> 321 <li>contentType(string, optional)</li> 322 */ 323 LABKEY.FileSystem.AbstractFileSystem = function(config){ 324 LABKEY.FileSystem.AbstractFileSystem.superclass.constructor.apply(this, arguments); 325 } 326 327 Ext.extend(LABKEY.FileSystem.AbstractFileSystem, Ext.util.Observable, { 328 329 /** 330 * Set to true if the fileSystem has loaded. 331 * @type Boolean 332 * @property 333 * @memberOf LABKEY.FileSystem.AbstractFileSystem# 334 */ 335 ready : true, 336 rootPath : "/", 337 separator : "/", 338 directoryMap : {}, 339 340 constructor : function(config) 341 { 342 Ext.util.Observable.prototype.constructor.call(this); 343 this.directoryMap = {}; 344 /** 345 * @memberOf LABKEY.FileSystem.AbstractFileSystem# 346 * @event 347 * @name ready 348 * @param {Filesystem} fileSystem A reference to the fileSystem 349 * @description Fires when the file system has loaded. 350 */ 351 /** 352 * @memberOf LABKEY.FileSystem.AbstractFileSystem# 353 * @event 354 * @name fileschanged 355 * @param {FileSystem} [fileSystem] A reference to the fileSystem. 356 * @param {String} [path] The path that was changed. 357 * @description Fires when the a path has been changed. 358 */ 359 /** 360 * @memberOf LABKEY.FileSystem.AbstractFileSystem# 361 * @event 362 * @name filesremoved 363 * @param {FileSystem} [fileSystem] A reference to the fileSystem. 364 * @param {Record[]} [records] An array of Ext.Record objects representing the files that were removed. These can be files and/or directories. 365 * @description Fires when one or more files or folders have been removed, either by a delete or move action. It is not fired when files are uncached for other reasons. 366 */ 367 this.addEvents( 368 LABKEY.FileSystem.FILESYSTEM_EVENTS.filesremoved, 369 LABKEY.FileSystem.FILESYSTEM_EVENTS.fileschanged, 370 LABKEY.FileSystem.FILESYSTEM_EVENTS.ready 371 ); 372 }, 373 374 /** 375 * Will list all the contents of the supplied path. If this path has already been loaded, the local cache will be used. 376 * @param config Configuration properties. 377 * @param {String} config.path The path to load 378 * @param {Function} config.success Success callback function. It will be called with the following arguments: 379 * <li>Filesystem: A reference to the filesystem</li> 380 * <li>Path: The path that was loaded</li> 381 * <li>Records: An array of record objects</li> 382 * @param {Function} [config.failure] Error callback function. It will be called with the following arguments: 383 * <li>Response: The XMLHttpRequest object containing the response data.</li> 384 * <li>Options: The parameter to the request call.</li> 385 * @param {Object} [config.scope] The scope for the callback functions 386 * @param {Boolean} [config.forceReload] If true, the path will always be reloaded instead of relying on the cache 387 * @methodOf LABKEY.FileSystem.AbstractFileSystem# 388 */ 389 listFiles : function(config) 390 { 391 config.scope = config.scope || this; 392 var files = this.directoryFromCache(config.path); 393 if (files && !config.forceReload) 394 { 395 if (typeof config.success == "function") 396 config.success.defer(1, config.scope, [this, config.path, files]); 397 } 398 else 399 { 400 this.reloadFiles(config); 401 } 402 }, 403 404 /** 405 * A helper to test if a file of the same name exists at a given path. If this path has not already been loaded, the local cache will be used unless forceReload is true. 406 * @param config Configuration properties. 407 * @param {String} config.name The name to test. This can either be a filename or a full path. If the latter is supplied, getFileName() will be used to extract the filename 408 * @param {String} config.path The path to check 409 * @param {Function} config.success Success callback function. It will be called with the following arguments: 410 * <li>Filesystem: A reference to the filesystem</li> 411 * <li>Name: The name to be tested</li> 412 * <li>Path: The path to be checked</li> 413 * <li>Record: If a record of the same name exists, the record object will be returned. Null indicates no name conflict exists</li> 414 * @param {Function} [config.failure] Error callback function. It will be called with the following arguments: 415 * <li>Response: The XMLHttpRequest object containing the response data.</li> 416 * <li>Options: The parameter to the request call.</li> 417 * @param {Object} [config.scope] The scope for the callback function. Defaults to 'this' 418 * @param {Boolean} [config.forceReload] If true, the cache will be reloaded prior to performing the check 419 * @methodOf LABKEY.FileSystem.AbstractFileSystem# 420 */ 421 checkForNameConflict: function(config) 422 { 423 var filename = this.concatPaths(config.path, this.getFileName(config.name)); 424 config.scope = config.scope || this; 425 426 this.listFiles({ 427 path: config.path, 428 success: function (fs, path, records) { 429 if (Ext.isFunction(config.success)) 430 config.success.defer(1, config.scope, [this, config.name, config.path, this.recordFromCache(filename)]); 431 }, 432 failure: config.failure, 433 scope: this, 434 forceReload: config.forceReload 435 }); 436 }, 437 438 /** 439 * Force reload on next listFiles call 440 * @ignore 441 * @param record 442 */ 443 uncacheListing : function(record) 444 { 445 var path = (typeof record == "string") ? record : record.data.path; 446 this.directoryMap[path] = null; 447 }, 448 449 /** 450 * @ignore 451 * @param record 452 */ 453 canRead : function(record) 454 { 455 return true; 456 }, 457 458 /** 459 * @ignore 460 * @param record 461 */ 462 canWrite: function(record) 463 { 464 return true; 465 }, 466 467 /** 468 * @ignore 469 * @param record 470 */ 471 canMkdir: function(record) 472 { 473 return true; 474 }, 475 476 /** 477 * @ignore 478 * @param record 479 */ 480 canDelete : function(record) 481 { 482 return true; 483 }, 484 485 /** 486 * @ignore 487 * @param record 488 */ 489 canMove : function(record) 490 { 491 return true; 492 }, 493 494 /** 495 * @ignore 496 * @param config 497 */ 498 deletePath : function(config) // callback(filesystem, success, path) 499 { 500 return false; 501 }, 502 503 /** 504 * @ignore 505 * @param config 506 */ 507 createDirectory : function(config) // callback(filesystem, success, path) 508 { 509 }, 510 511 /** 512 * Called by listFiles(), return false on immediate fail 513 * @ignore 514 */ 515 reloadFiles : function(config) 516 { 517 return false; 518 }, 519 520 /** 521 * @ignore 522 * @param config 523 */ 524 getHistory : function(config) // callback(filesystem, success, path, history[]) 525 { 526 }, 527 528 // protected 529 530 _addFiles : function(path, records) 531 { 532 this.directoryMap[path] = records; 533 this.fireEvent(LABKEY.FileSystem.FILESYSTEM_EVENTS.fileschanged, this, path, records); 534 }, 535 536 /** 537 * For a supplied path, returns an array corresponding Ext Record from the cache 538 * @param {String} path The path of the directory 539 * @returns {Ext.Record[]} An array of Ext.Records representing the contents of the directory. Returns null if the directory is not in the cache. 540 * @methodOf LABKEY.FileSystem.AbstractFileSystem# 541 * @name directoryFromCache 542 */ 543 directoryFromCache : function(path) 544 { 545 var files = this.directoryMap[path]; 546 if (!files && path && path.length>0 && path.charAt(path.length-1) == this.separator) 547 path = path.substring(0,path.length-1); 548 files = this.directoryMap[path]; 549 return files; 550 }, 551 552 /** 553 * For a supplied path, returns the corresponding Ext Record from the cache 554 * @param {String} path The path of the file or directory 555 * @returns {Ext.Record} The Ext.Record for this file. Returns null if the file is not found. 556 * @methodOf LABKEY.FileSystem.AbstractFileSystem# 557 * @name recordFromCache 558 */ 559 recordFromCache : function(path) 560 { 561 if (!path || path == this.rootPath) 562 return this.rootRecord; 563 var parent = this.getParentPath(path) || this.rootPath; 564 var name = this.getFileName(path); 565 var files = this.directoryFromCache(parent); 566 if (!files) 567 return null; 568 for (var i=0 ; i<files.length ; i++) 569 { 570 var r = files[i]; 571 if (r.data.name == name) 572 return r; 573 } 574 return null; 575 }, 576 577 onReady : function(fn) 578 { 579 if (this.ready) 580 fn.call(); 581 else 582 this.on(LABKEY.FileSystem.FILESYSTEM_EVENTS.ready, fn); 583 }, 584 585 // util 586 587 /** 588 * A utility method to concatenate 2 strings into a normalized filepath 589 * @param {String} a The first path 590 * @param {String} b The first path 591 * @returns {String} The concatenated path 592 * @methodOf LABKEY.FileSystem.AbstractFileSystem# 593 */ 594 concatPaths : function(a,b) 595 { 596 var c = 0; 597 if (a.length > 0 && a.charAt(a.length-1)==this.separator) c++; 598 if (b.length > 0 && b.charAt(0)==this.separator) c++; 599 if (c == 0) 600 return a + this.separator + b; 601 else if (c == 1) 602 return a + b; 603 else 604 return a + b.substring(1); 605 }, 606 607 /** 608 * A utility method to extract the parent path from a file or folder path 609 * @param {String} p The path to the file or directory 610 * @returns {String} The parent path 611 * @methodOf LABKEY.FileSystem.AbstractFileSystem# 612 */ 613 getParentPath : function(p) 614 { 615 if (!p) 616 p = this.rootPath; 617 if (p.length > 1 && p.charAt(p.length-1) == this.separator) 618 p = p.substring(0,p.length-1); 619 var i = p.lastIndexOf(this.separator); 620 return i == -1 ? this.rootPath : p.substring(0,i+1); 621 }, 622 623 /** 624 * A utility method to extract the filename from a file path. 625 * @param {String} p The path to the file or directory 626 * @returns {String} The file name 627 * @methodOf LABKEY.FileSystem.AbstractFileSystem# 628 */ 629 getFileName : function(p) 630 { 631 if (!p || p == this.rootPath) 632 return this.rootPath; 633 if (p.length > 1 && p.charAt(p.length-1) == this.separator) 634 p = p.substring(0,p.length-1); 635 var i = p.lastIndexOf(this.separator); 636 if (i > -1) 637 p = p.substring(i+1); 638 return p; 639 }, 640 641 /** 642 * A utility to test if a path is a direct child of another path 643 * @param {String} a The first path to test 644 * @param {String} b The second path to test 645 * @returns {Boolean} Returns true if the first path is a direct child of the second 646 * @methodOf LABKEY.FileSystem.AbstractFileSystem# 647 */ 648 isChild: function(a, b){ 649 return a.indexOf(b) == 0; 650 //return a.match(new RegExp('^' + b + '.+', 'i')); 651 } 652 653 }); 654 655 656 /** 657 * This class enables interaction with WebDav filesystems, such as the one exposed through LabKey Server. 658 * In addition to the properties and methods documented here, all methods from LABKEY.FileSystem.AbstractFileSystem are available. 659 * @class LABKEY.FileSystem.WebdavFileSystem 660 * @augments LABKEY.FileSystem.AbstractFileSystem 661 * @constructor 662 * @param config Configuration properties. 663 * @param {String} [config.containerPath] The path to the container to load (ie. '/home') 664 * @param {String} [config.filePath] The file path, relative to the containerPath (ie. '/mySubfolder'). Optional. 665 * @param {String} [config.fileLink] A folder name that is appended after the container and filePath. By default, LabKey stores files in a subfolder called '/@files'. If the pipeline root of your container differs from the file root, the files may be stored in '/@pipeline'. This defaults to '/@files'. To omit this, use 'fileLink: null'. For non-standard URLs, also see config.baseUrl. 666 * @param {string} [config.baseUrl] Rather than supplying a containerPath and filePath, you can supply the entire baseUrl to use as the root of the webdav tree (ie. http://localhost:8080/labkey/_webdav/home/@files/), must be an ABSOLUTE URL ("/_webdav" NOT "_webdav"). If provided, this will be preferentially used instead of creating a baseUrl from containerPath, filePath and fileLink 667 * @param {string} [config.rootName] The display name for the root (ie. 'Fileset'). Optional. 668 * @param {array} [config.extraDataFields] An array of extra Ext.data.Field config objects that will be appended to the FileRecord. Optional. 669 * @example <script type="text/javascript"> 670 671 //this example loads files from: /_webdav/home/mySubfolder/@files 672 var fileSystem = new LABKEY.FileSystem.WebdavFileSystem({ 673 containerPath: '/home', 674 filePath: '/mySubfolder' //optional. 675 }); 676 677 //this would be identical to the example above 678 new LABKEY.FileSystem.WebdavFileSystem({ 679 baseUrl: "/_webdav/home/mySubfolder/@files" 680 }); 681 682 //this creates the URL: /webdav/myProject/@pipeline 683 new LABKEY.FileSystem.WebdavFileSystem({ 684 containerPath: '/myProject', 685 fileLink: '/@pipeline' 686 }); 687 688 689 fileSystem.on('ready', function(fileSystem){ 690 fileSystem.listFiles({ 691 path: '/mySubfolder', 692 success: function(fileSystem, path, records){ 693 alert('It worked!'); 694 console.log(records); 695 }, 696 scope: this 697 }, this); 698 699 fileSystem.movePath({ 700 source: '/myFile.xls', 701 destination: '/foo/myFile.xls', 702 isFile: true, 703 success: function(fileSystem, path, records){ 704 alert('It worked!'); 705 }, 706 failure: function(response, options){ 707 alert('It didnt work. The error was ' + response.statusText); 708 console.log(response); 709 }, 710 scope: this 711 }); 712 }, this); 713 714 715 </script> 716 */ 717 LABKEY.FileSystem.WebdavFileSystem = function(config) 718 { 719 config = config || {}; 720 Ext.apply(this, config, { 721 baseUrl: LABKEY.contextPath + "/_webdav", 722 rootPath: "/", 723 rootName : (LABKEY.serverName || "LabKey Server"), 724 fileLink: "/@files" 725 }); 726 this.ready = false; 727 this.initialConfig = config; 728 729 LABKEY.FileSystem.WebdavFileSystem.superclass.constructor.call(this); 730 731 this.HistoryRecord = Ext.data.Record.create(['user', 'date', 'message', 'href']); 732 this.historyReader = new Ext.data.XmlReader({record : "entry"}, this.HistoryRecord); 733 734 this.init(config); 735 this.reloadFile("/", (function() 736 { 737 this.ready = true; 738 this.fireEvent(LABKEY.FileSystem.FILESYSTEM_EVENTS.ready, this); 739 }).createDelegate(this)); 740 }; 741 742 Ext.extend(LABKEY.FileSystem.WebdavFileSystem, LABKEY.FileSystem.AbstractFileSystem, 743 { 744 /** 745 * Returns the history for the file or directory at the supplied path 746 * @param config Configuration properties. 747 * @param {String} config.path Path to the file or directory 748 * @param {Function} config.success Success callback function. It will be called with the following arguments: 749 * <li>Filesystem: A reference to the filesystem</li> 750 * <li>Path: The path that was loaded</li> 751 * <li>History: An array of records representing the history</li> 752 * @param {Function} [config.failure] Error callback function. It will be called with the following arguments: 753 * <li>Response: the response object</li> 754 * @param {Object} [config.scope] The scope of the callback function 755 * @methodOf LABKEY.FileSystem.WebdavFileSystem# 756 */ 757 getHistory : function(config) 758 { 759 config.scope = config.scope || this; 760 var body = "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<propfind xmlns=\"DAV:\"><prop><history/></prop></propfind>"; 761 762 var proxy = new Ext.data.HttpProxy( 763 { 764 url: this.concatPaths(this.prefixUrl, config.path), 765 xmlData : body, 766 method: "PROPFIND", 767 headers: {"Depth" : "0"} 768 }); 769 proxy.api.read.method = 'PROPFIND'; 770 771 var cb = function(response, args, success) 772 { 773 LABKEY.FileSystem.Util._processAjaxResponse(response); 774 if (success && Ext.isFunction(config.success)) 775 config.success.call(config.scope, args.filesystem, args.path, response.records); 776 else if (!success && Ext.isFunction(config.failure)) 777 config.failure.call(config.scope, response); 778 }; 779 proxy.request('read', null, {method:"PROPFIND", depth:"0", propname : this.propNames}, this.historyReader, cb, this, {filesystem:this, path:config.path}); 780 }, 781 782 /** 783 * Returns true if the current user can read the passed file 784 * In order to obtain the record for the desired file, recordFromCache() is normally used. 785 * @param {Ext.Record} record The Ext record associated with the file. See LABKEY.AbstractFileSystem.FileRecord for more information. 786 * @methodOf LABKEY.FileSystem.WebdavFileSystem# 787 */ 788 canRead : function(record) 789 { 790 var options = record.data.options; 791 return !options || -1 != options.indexOf('GET'); 792 }, 793 794 /** 795 * Returns true if the current user can write to the passed file or location 796 * In order to obtain the record for the desired file, recordFromCache() is normally used. 797 * @param {Ext.Record} record The Ext record associated with the file. See LABKEY.AbstractFileSystem.FileRecord for more information. 798 * @methodOf LABKEY.FileSystem.WebdavFileSystem# 799 */ 800 canWrite : function(record) 801 { 802 var options = record.data.options; 803 return !options || -1 != options.indexOf("PUT"); 804 }, 805 806 /** 807 * Returns true if the current user can create a folder in the passed location 808 * In order to obtain the record for the desired file, recordFromCache() is normally used. 809 * @param {Ext.Record} record The Ext record associated with the file. See LABKEY.AbstractFileSystem.FileRecord for more information. 810 * @methodOf LABKEY.FileSystem.WebdavFileSystem# 811 */ 812 canMkdir : function(record) 813 { 814 var options = record.data.options; 815 return !options || -1 != options.indexOf("MKCOL"); 816 }, 817 818 /** 819 * Returns true if the current user can delete the passed file. 820 * In order to obtain the record for the desired file, recordFromCache() is normally used. 821 * @param {Ext.Record} record The Ext record associated with the file. See LABKEY.AbstractFileSystem.FileRecord for more information. 822 * @methodOf LABKEY.FileSystem.WebdavFileSystem# 823 */ 824 canDelete : function(record) 825 { 826 var options = record.data.options; 827 return !options || -1 != options.indexOf('DELETE'); 828 }, 829 830 /** 831 * Returns true if the current user can move or rename the passed file 832 * In order to obtain the record for the desired file, recordFromCache() is normally used. 833 * @param {Ext.Record} record The Ext record associated with the file. See LABKEY.AbstractFileSystem.FileRecord for more information. 834 * @methodOf LABKEY.FileSystem.WebdavFileSystem# 835 */ 836 canMove : function(record) 837 { 838 var options = record.data.options; 839 return !options || -1 != options.indexOf('MOVE'); 840 }, 841 842 /** 843 * Can be used to delete a file or folder. 844 * @param config Configuration properties. 845 * @param {String} config.path The source file, which should be a URL relative to the fileSystem's rootPath 846 * @param {Boolean} config.isFile Set to true is this represent a file, as opposed to a folder 847 * @param {Function} config.success Success callback function. It will be called with the following arguments: 848 * <li>Filesystem: A reference to the filesystem</li> 849 * <li>Path: The path that was loaded</li> 850 * @param {Object} [config.failure] The error callback function. It will be called with the following arguments: 851 * <li>Response: The XMLHttpRequest object containing the response data.</li> 852 * <li>Options: The parameter to the request call.</li> 853 * @param {Object} [config.scope] The scope of the callback functions 854 * @methodOf LABKEY.FileSystem.WebdavFileSystem# 855 */ 856 deletePath : function(config) 857 { 858 config.scope = config.scope || this; 859 var resourcePath = this.concatPaths(this.prefixUrl, LABKEY.ActionURL.encodePath(config.path)); 860 var fileSystem = this; 861 var connection = new Ext.data.Connection(); 862 863 connection.request({ 864 method: "DELETE", 865 url: resourcePath, 866 scope: this, 867 success: function(response, options){ 868 var success = false; 869 LABKEY.FileSystem.Util._processAjaxResponse(response); 870 if (204 == response.status || 404 == response.status) // NO_CONTENT (success) 871 success = true; 872 else if (405 == response.status) // METHOD_NOT_ALLOWED 873 success = false; 874 875 if (success) 876 { 877 fileSystem._deleteListing(config.path, config.isFile); 878 879 if (Ext.isFunction(config.success)) 880 config.success.call(config.scope, fileSystem, config.path); 881 } 882 else { 883 if (Ext.isFunction(config.failure)) 884 config.failure.call(config.scope, response, options); 885 } 886 }, 887 failure: function(response, options) 888 { 889 var success = false; 890 LABKEY.FileSystem.Util._processAjaxResponse(response); 891 if (response.status == 404) //NOT_FOUND - not sure if this is the correct behavior or not 892 success = true; 893 894 if (!success && Ext.isFunction(config.failure)) 895 config.failure.call(config.scope, response, options); 896 if (success && Ext.isFunction(config.success)) 897 config.success.call(config.scope, fileSystem, config.path); 898 }, 899 headers: { 900 'Content-Type': 'application/json' 901 } 902 }); 903 904 return true; 905 }, 906 907 //private 908 _deleteListing: function(path, isFile) 909 { 910 var deleted = []; 911 912 //always delete the record itself 913 var record = this.recordFromCache(path); 914 if (record) { 915 isFile = record.data.file; 916 deleted.push(record); 917 var parentPath = this.getParentPath(path); 918 var parentFolder = this.directoryMap[parentPath]; 919 if (parentFolder){ 920 parentFolder.remove(record); 921 } 922 } 923 924 // find all paths modified by this delete 925 if (!isFile) 926 { 927 var pathsRemoved = []; 928 for (var a in this.directoryMap) 929 { 930 if (typeof a == 'string') 931 { 932 var idx = a.indexOf(path); 933 if (idx == 0) 934 { 935 pathsRemoved.push(a); 936 var r = this.recordFromCache(a); 937 if (r) 938 deleted.push(r); 939 940 parentPath = this.getParentPath(a); 941 parentFolder = this.directoryMap[parentPath]; 942 if (parentFolder && r){ 943 parentFolder.remove(r); 944 } 945 if (this.directoryMap[a] && this.directoryMap[a].length) 946 deleted = deleted.concat(this.directoryMap[a]); 947 } 948 } 949 } 950 this.uncacheListing(path); 951 } 952 953 deleted = Ext.unique(deleted); 954 this.fireEvent(LABKEY.FileSystem.FILESYSTEM_EVENTS.filesremoved, this, path, deleted); 955 }, 956 957 /** 958 * Can be used to rename a file or folder. This is simply a convenience wrapper for movePath(). 959 * @param config Configuration properties. 960 * @param {String} config.source The source file, which should be relative to the fileSystem's rootPath 961 * @param {String} config.destination The target path, which should be the full path for the new file, relative to the fileSystem's rootPath 962 * @param {Boolean} config.isFile Set to true if the path is a file, as opposed to a directory 963 * @param {Function} config.success Success callback function. It will be called with the following arguments: 964 * <li>Filesystem: A reference to the filesystem</li> 965 * <li>SourcePath: The path to the file/folder to be renamed</li> 966 * <li>DestPath: The new path for the renamed file/folder</li> 967 * @param {Object} [config.failure] The failure callback function. Will be called with the following arguments: 968 * <li>Response: The XMLHttpRequest object containing the response data.</li> 969 * <li>Options: The parameter to the request call.</li> 970 * @param {Object} [config.scope] The scope of the callback function 971 * @param {Boolean} [config.overwrite] If true, files at the target location 972 * @methodOf LABKEY.FileSystem.WebdavFileSystem# 973 * @example <script type="text/javascript"> 974 var fileSystem = new new LABKEY.FileSystem.WebdavFileSystem({ 975 containerPath: '/home', 976 filePath: '/@files' //optional. this is the same as the default 977 }); 978 979 fileSystem.on('ready', function(fileSystem){ 980 fileSystem.listFiles({ 981 path: '/mySubfolder/', 982 success: function(fileSystem, path, records){ 983 alert('It worked!'); 984 console.log(records); 985 }, 986 scope: this 987 }, this); 988 989 fileSystem.renamePath({ 990 source: 'myFile.xls', 991 destination: 'renamedFile.xls', 992 isFile: true, 993 scope: this 994 }); 995 996 997 //if you renamed a file in a subfolder, you can optionally supply the fileName only 998 //this file will be renamed to: '/subfolder/renamedFile.xls' 999 fileSystem.renamePath({ 1000 source: '/subfolder/myFile.xls', 1001 destination: 'renamedFile.xls', 1002 isFile: true, 1003 scope: this 1004 }); 1005 1006 //or provide the entire path 1007 fileSystem.renamePath({ 1008 source: '/subfolder/myFile.xls', 1009 destination: '/subfolder/renamedFile.xls', 1010 isFile: true, 1011 scope: this 1012 }); 1013 }, this); 1014 1015 1016 </script> 1017 */ 1018 renamePath : function(config) 1019 { 1020 //allow user to submit either full path for rename, or just the new filename 1021 if (config.source.indexOf(this.separator) > -1 && config.destination.indexOf(this.separator) == -1){ 1022 config.destination = this.concatPaths(this.getParentPath(config.source), config.destination); 1023 } 1024 1025 this.movePath({ 1026 source: config.source, 1027 destination: config.destination, 1028 isFile: config.isFile, 1029 success: config.success, 1030 failure: config.failure, 1031 scope: config.scope, 1032 overwrite: config.overwrite 1033 }); 1034 }, 1035 1036 /** 1037 * Can be used to move a file or folder from one location to another. 1038 * @param config Configuration properties. 1039 * @param {String} config.path The source file, which should be a URL relative to the fileSystem's rootPath 1040 * @param {String} config.destination The target path, which should be a URL relative to the fileSystem's rootPath 1041 * @param {Boolean} config.isFile True if the file to move is a file, as opposed to a directory 1042 * @param {Function} config.success Success callback function. It will be called with the following arguments: 1043 * <li>Filesystem: A reference to the filesystem</li> 1044 * <li>SourcePath: The path that was loaded</li> 1045 * <li>DestPath: The path that was loaded</li> 1046 * @param {Object} [config.failure] The failure callback function. Will be called with the following arguments: 1047 * <li>Response: The XMLHttpRequest object containing the response data.</li> 1048 * <li>Options: The parameter to the request call.</li> 1049 * @param {Object} [config.scope] The scope of the callbacks 1050 * @param {Boolean} [config.overwrite] If true, files at the target location 1051 * @methodOf LABKEY.FileSystem.WebdavFileSystem# 1052 */ 1053 movePath : function(config) 1054 { 1055 config.scope = config.scope || this; 1056 1057 var resourcePath = this.concatPaths(this.prefixUrl, LABKEY.ActionURL.encodePath(config.source)); 1058 var destinationPath = this.concatPaths(this.prefixUrl, LABKEY.ActionURL.encodePath(config.destination)); 1059 var fileSystem = this; 1060 var connection = new Ext.data.Connection(); 1061 1062 var cfg = { 1063 method: "MOVE", 1064 url: resourcePath, 1065 scope: this, 1066 failure: function(response){ 1067 LABKEY.FileSystem.Util._processAjaxResponse(response); 1068 if(Ext.isFunction(config.failure)) 1069 config.failure.apply(config.scope, arguments); 1070 }, 1071 success: function(response, options){ 1072 LABKEY.FileSystem.Util._processAjaxResponse(response); 1073 var success = false; 1074 if (201 == response.status || 204 == response.status) //CREATED, NO_CONTENT (success) 1075 success = true; 1076 else 1077 success = false; 1078 1079 if (success) 1080 { 1081 //the move is performed as a delete / lazy-insert 1082 fileSystem._deleteListing(config.source, config.isFile); 1083 1084 var destParent = fileSystem.getParentPath(config.destination); 1085 fileSystem.uncacheListing(destParent); //this will cover uncaching children too 1086 1087 // TODO: maybe support a config option that will to force the fileSystem to 1088 // auto-reload this location, instead just uncaching and relying on consumers to do it?? 1089 this.fireEvent(LABKEY.FileSystem.FILESYSTEM_EVENTS.fileschanged, this, destParent); 1090 1091 if (Ext.isFunction(config.success)) 1092 config.success.call(config.scope, fileSystem, config.source, config.destination); 1093 } 1094 else { 1095 if (Ext.isFunction(config.failure)) 1096 config.failure.call(config.scope, response, options); 1097 } 1098 }, 1099 headers: { 1100 Destination: destinationPath, 1101 'Content-Type': 'application/json' 1102 } 1103 }; 1104 1105 if (config.overwrite) 1106 cfg.headers.Overwrite = 'T'; 1107 1108 connection.request(cfg); 1109 1110 return true; 1111 }, 1112 1113 /** 1114 * Will create a directory at the provided location. This does not perform permission checking, which can be done using canMkDir(). 1115 * @param config Configuration properties. 1116 * @param {String} config.path The path of the folder to create. This should be relative to the rootPath of the FileSystem. See constructor for examples. 1117 * @param {Function} config.success Success callback function. It will be called with the following arguments: 1118 * <li>Filesystem: A reference to the filesystem</li> 1119 * <li>Path: The path that was created</li> 1120 * @param {Object} [config.failure] Failure callback function. It will be called with the following arguments: 1121 * <li>Response: the response object</li> 1122 * <li>Options: The parameter to the request call.</li> 1123 * @param {Object} [config.scope] The scope of the callback functions. 1124 * @methodOf LABKEY.FileSystem.WebdavFileSystem# 1125 */ 1126 createDirectory : function(config) 1127 { 1128 var fileSystem = this; 1129 config.scope = config.scope || this; 1130 1131 var resourcePath = this.concatPaths(this.prefixUrl, config.path); 1132 var connection = new Ext.data.Connection(); 1133 1134 connection.request({ 1135 method: "MKCOL", 1136 url: resourcePath, 1137 scope: this, 1138 success: function(response, options){ 1139 LABKEY.FileSystem.Util._processAjaxResponse(response); 1140 var success = false; 1141 if (200 == response.status || 201 == response.status){ // OK, CREATED 1142 if(!response.responseText) 1143 success = true; 1144 else 1145 success = false; 1146 } 1147 else if (405 == response.status) // METHOD_NOT_ALLOWED 1148 success = false; 1149 1150 if (success && Ext.isFunction(config.success)) 1151 config.success.call(config.scope, fileSystem, config.path); 1152 if (!success && Ext.isFunction(config.failure)) 1153 config.failure.call(config.scope, response, options); 1154 }, 1155 failure: function(response){ 1156 LABKEY.FileSystem.Util._processAjaxResponse(response); 1157 if (Ext.isFunction(config.failure)) 1158 config.failure.apply(config.scope, arguments); 1159 }, 1160 headers: { 1161 'Content-Type': 'application/json' 1162 } 1163 }); 1164 1165 return true; 1166 }, 1167 1168 //private 1169 // not sure why both this and reloadFiles() exist? reloadFile() seems to be used internally only 1170 reloadFile : function(path, callback) 1171 { 1172 var url = this.concatPaths(this.prefixUrl, LABKEY.ActionURL.encodePath(path)); 1173 this.connection.url = url; 1174 var args = {url: url, path: path, callback:callback}; 1175 this.proxy.doRequest("read", null, {method:"PROPFIND",depth:"0", propname : this.propNames}, this.transferReader, this.processFile, this, args); 1176 return true; 1177 }, 1178 1179 //private 1180 _updateRecord : function(update) 1181 { 1182 var path = update.data.path; 1183 if (path == '/') 1184 { 1185 Ext.apply(this.rootRecord.data, update.data); 1186 } 1187 else 1188 { 1189 var record = this.recordFromCache(path); 1190 if (record) 1191 Ext.apply(record.data, update.data); 1192 } 1193 }, 1194 1195 //private 1196 processFile : function(result, args, success) 1197 { 1198 var update = null; 1199 if (success && result && !Ext.isArray(result.records)) 1200 success = false; 1201 if (success && result.records.length == 1) 1202 { 1203 update = result.records[0]; 1204 this._updateRecord(update); 1205 } 1206 1207 if (Ext.isFunction(args.callback)) 1208 args.callback(this, success && null != update, args.path, update); 1209 }, 1210 1211 //private 1212 uncacheListing : function(record) 1213 { 1214 var path = (typeof record == "string") ? record : record.data.path; 1215 1216 // want to uncache all subfolders of the parent folder 1217 Ext.iterate(this.directoryMap, function(key, value) { 1218 if (Ext.isString(key)) { 1219 if (key.indexOf(path) === 0) { 1220 this.directoryMap[key] = null; 1221 } 1222 } 1223 }, this); 1224 1225 var args = this.pendingPropfind[path]; 1226 if (args && args.transId) 1227 { 1228 this.connection.abort(args.transId); 1229 this.connection.url = args.url; 1230 this.proxy.doRequest("read", null, {method:"PROPFIND",depth:"1", propname : this.propNames}, this.transferReader, this.processFiles, this, args); 1231 args.transId = this.connection.transId; 1232 } 1233 }, 1234 1235 //private 1236 reloadFiles : function(config) 1237 { 1238 config.scope = config.scope || this; 1239 1240 var cb = { 1241 success: config.success, 1242 failure: config.failure, 1243 scope: config.scope 1244 }; 1245 1246 var args = this.pendingPropfind[config.path]; 1247 if (args) 1248 { 1249 args.callbacks.push(cb); 1250 return; 1251 } 1252 1253 var url = this.concatPaths(this.prefixUrl, LABKEY.ActionURL.encodePath(config.path)); 1254 this.connection.url = url; 1255 this.pendingPropfind[config.path] = args = {url: url, path: config.path, callbacks:[cb]}; 1256 this.proxy.doRequest("read", null, {method:"PROPFIND",depth:"1", propname : this.propNames}, this.transferReader, this.processFiles, this, args); 1257 args.transId = this.connection.transId; 1258 return true; 1259 }, 1260 1261 //private 1262 processFiles : function(result, args, success) 1263 { 1264 delete this.pendingPropfind[args.path]; 1265 1266 var path = args.path; 1267 1268 var directory = null; 1269 var listing = []; 1270 if (success && result && !Ext.isArray(result.records)) 1271 success = false; 1272 if (success) 1273 { 1274 var records = result.records; 1275 for (var r=0 ; r<records.length ; r++) 1276 { 1277 var record = records[r]; 1278 if (record.data.path == path) 1279 directory = record; 1280 else 1281 listing.push(record); 1282 } 1283 if (directory) 1284 this._updateRecord(directory); 1285 this._addFiles(path, listing); 1286 } 1287 1288 var callbacks = args.callbacks; 1289 for (var i=0 ; i<callbacks.length ; i++) 1290 { 1291 var callback = callbacks[i]; 1292 if (Ext.isFunction(callback)) { 1293 callback(this, path, listing); 1294 } 1295 else if (typeof callback == 'object') { 1296 var scope = callback.scope || this; 1297 if (success && Ext.isFunction(callback.success)) 1298 callback.success.call(scope, this, path, listing); 1299 else if (!success && Ext.isFunction(callback.failure)) 1300 callback.failure.call(scope, args.transId.conn); 1301 } 1302 } 1303 }, 1304 1305 //private 1306 init : function(config) 1307 { 1308 //support either containerPath + path OR baseUrl (which is the concatenation of these 2) 1309 this.filePath = this.filePath || ''; 1310 this.fileLink = this.fileLink || ''; 1311 this.containerPath = this.containerPath || LABKEY.ActionURL.getContainer(); 1312 1313 if (!config.baseUrl){ 1314 this.baseUrl = this.concatPaths(LABKEY.contextPath + "/_webdav", encodeURI(this.containerPath)); 1315 this.baseUrl = this.concatPaths(this.baseUrl, encodeURI(this.filePath)); 1316 this.baseUrl = this.concatPaths(this.baseUrl, encodeURI(this.fileLink)); 1317 } 1318 1319 var prefix = this.concatPaths(this.baseUrl, this.rootPath); 1320 if (prefix.length > 0 && prefix.charAt(prefix.length-1) == this.separator) 1321 prefix = prefix.substring(0,prefix.length-1); 1322 this.prefixUrl = prefix; 1323 this.pendingPropfind = {}; 1324 1325 var prefixDecode = decodeURIComponent(prefix); 1326 1327 var getURI = function(v,rec) 1328 { 1329 var uri = rec.uriOBJECT || new LABKEY.URI(v); 1330 if (!Ext.isIE && !rec.uriOBJECT) 1331 try {rec.uriOBJECT = uri;} catch (e) {}; 1332 return uri; 1333 }; 1334 1335 this.propNames = ["creationdate", "displayname", "createdby", "getlastmodified", "modifiedby", "getcontentlength", 1336 "getcontenttype", "getetag", "resourcetype", "source", "path", "iconHref", "options"]; 1337 1338 if (config.extraPropNames && config.extraPropNames.length) 1339 this.propNames = this.propNames.concat(config.extraPropNames); 1340 1341 var recordCfg = [ 1342 {name: 'uri', mapping: 'href', 1343 convert : function(v, rec) 1344 { 1345 var uri = getURI(v,rec); 1346 return uri ? uri.href : ""; 1347 } 1348 }, 1349 {name: 'fileLink', mapping: 'href', 1350 convert : function(v, rec) 1351 { 1352 var uri = getURI(v,rec); 1353 1354 if (uri && uri.file) 1355 return Ext.DomHelper.markup({ 1356 tag :'a', 1357 href : Ext.util.Format.htmlEncode(uri.href + '?contentDisposition=attachment'), 1358 html : Ext.util.Format.htmlEncode(decodeURIComponent(uri.file))}); 1359 else 1360 return ''; 1361 } 1362 }, 1363 {name: 'path', mapping: 'href', 1364 convert : function (v, rec) 1365 { 1366 var uri = getURI(v,rec); 1367 var path = decodeURIComponent(uri.pathname); 1368 if (path.length >= prefixDecode.length && path.substring(0,prefixDecode.length) == prefixDecode) 1369 path = path.substring(prefixDecode.length); 1370 return path; 1371 } 1372 }, 1373 {name: 'name', mapping: 'propstat/prop/displayname', sortType:'asUCString'}, 1374 {name: 'fileExt', mapping: 'propstat/prop/displayname', 1375 convert : function (v, rec) 1376 { 1377 // parse the file extension from the file name 1378 var idx = v.lastIndexOf('.'); 1379 if (idx != -1) 1380 return v.substring(idx+1); 1381 return ''; 1382 } 1383 }, 1384 1385 {name: 'file', mapping: 'href', type: 'boolean', 1386 convert : function (v, rec) 1387 { 1388 // UNDONE: look for <collection> 1389 var uri = getURI(v, rec); 1390 var path = uri.pathname; 1391 return path.length > 0 && path.charAt(path.length-1) != '/'; 1392 } 1393 }, 1394 {name: 'created', mapping: 'propstat/prop/creationdate', type: 'date', dateFormat : "c"}, 1395 {name: 'createdBy', mapping: 'propstat/prop/createdby'}, 1396 {name: 'modified', mapping: 'propstat/prop/getlastmodified', type: 'date'}, 1397 {name: 'modifiedBy', mapping: 'propstat/prop/modifiedby'}, 1398 {name: 'size', mapping: 'propstat/prop/getcontentlength', type: 'int'}, 1399 {name: 'iconHref'}, 1400 {name: 'contentType', mapping: 'propstat/prop/getcontenttype'}, 1401 {name: 'options'} 1402 ]; 1403 1404 if (config.extraDataFields && config.extraDataFields.length) 1405 recordCfg = recordCfg.concat(config.extraDataFields); 1406 1407 this.FileRecord = Ext.data.Record.create(recordCfg); 1408 this.connection = new Ext.data.Connection({method: "GET", timeout: 600000, headers: {"Method" : "PROPFIND", "Depth" : "1", propname : this.propNames}}); 1409 this.proxy = new Ext.data.HttpProxy(this.connection); 1410 this.transferReader = new Ext.data.XmlReader({record : "response", id : "href"}, this.FileRecord); 1411 1412 this.rootRecord = new this.FileRecord({ 1413 id:"/", 1414 path:"/", 1415 name: this.rootName, 1416 file:false, 1417 uri:this.prefixUrl, 1418 iconHref: LABKEY.contextPath + "/_images/labkey.png" 1419 }, "/"); 1420 } 1421 }); 1422 1423 1424 1425