1 /*
  2  * Copyright (c) 2012-2019 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     <li>absolutePath(string, optional)</li>
323  */
324 LABKEY.FileSystem.AbstractFileSystem = function(config){
325     LABKEY.FileSystem.AbstractFileSystem.superclass.constructor.apply(this, arguments);
326 };
327 
328 Ext.extend(LABKEY.FileSystem.AbstractFileSystem, Ext.util.Observable, {
329 
330     /**
331      * Set to true if the fileSystem has loaded.
332      * @type Boolean
333      * @property
334      * @memberOf LABKEY.FileSystem.AbstractFileSystem#
335      */
336     ready     : true,
337     rootPath  : "/",
338     separator : "/",
339     directoryMap : {},
340 
341     constructor : function(config)
342     {
343         Ext.util.Observable.prototype.constructor.call(this);
344         this.directoryMap = {};
345         /**
346          * @memberOf LABKEY.FileSystem.AbstractFileSystem#
347          * @event
348          * @name ready
349          * @param {Filesystem} fileSystem A reference to the fileSystem
350          * @description Fires when the file system has loaded.
351          */
352         /**
353          * @memberOf LABKEY.FileSystem.AbstractFileSystem#
354          * @event
355          * @name fileschanged
356          * @param {FileSystem} [fileSystem] A reference to the fileSystem.
357          * @param {String} [path] The path that was changed.
358          * @description Fires when the a path has been changed.
359          */
360         /**
361          * @memberOf LABKEY.FileSystem.AbstractFileSystem#
362          * @event
363          * @name filesremoved
364          * @param {FileSystem} [fileSystem] A reference to the fileSystem.
365          * @param {Record[]} [records] An array of Ext.Record objects representing the files that were removed.  These can be files and/or directories.
366          * @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.
367          */
368         this.addEvents(
369             LABKEY.FileSystem.FILESYSTEM_EVENTS.filesremoved,
370             LABKEY.FileSystem.FILESYSTEM_EVENTS.fileschanged,
371             LABKEY.FileSystem.FILESYSTEM_EVENTS.ready
372         );
373     },
374 
375     /**
376      * Will list all the contents of the supplied path.  If this path has already been loaded, the local cache will be used.
377      * @param config Configuration properties.
378      * @param {String} config.path The path to load
379      * @param {Function} config.success Success callback function.  It will be called with the following arguments:
380      * <li>Filesystem: A reference to the filesystem</li>
381      * <li>Path: The path that was loaded</li>
382      * <li>Records: An array of record objects</li>
383      * @param {Function} [config.failure] Error callback function.  It will be called with the following arguments:
384      * <li>Response: The XMLHttpRequest object containing the response data.</li>
385      * <li>Options: The parameter to the request call.</li>
386      * @param {Object} [config.scope] The scope for the callback functions
387      * @param {Boolean} [config.forceReload] If true, the path will always be reloaded instead of relying on the cache
388      * @methodOf LABKEY.FileSystem.AbstractFileSystem#
389      */
390     listFiles : function(config)
391     {
392         config.scope = config.scope || this;
393         var files = this.directoryFromCache(config.path);
394         if (files && !config.forceReload)
395         {
396             if (typeof config.success == "function")
397                 config.success.defer(1, config.scope, [this, config.path, files]);
398         }
399         else
400         {
401             this.reloadFiles(config);
402         }
403     },
404 
405     /**
406      * 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.
407      * @param config Configuration properties.
408      * @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
409      * @param {String} config.path The path to check
410      * @param {Function} config.success Success callback function.  It will be called with the following arguments:
411      * <li>Filesystem: A reference to the filesystem</li>
412      * <li>Name: The name to be tested</li>
413      * <li>Path: The path to be checked</li>
414      * <li>Record: If a record of the same name exists, the record object will be returned.  Null indicates no name conflict exists</li>
415      * @param {Function} [config.failure] Error callback function.  It will be called with the following arguments:
416      * <li>Response: The XMLHttpRequest object containing the response data.</li>
417      * <li>Options: The parameter to the request call.</li>
418      * @param {Object} [config.scope] The scope for the callback function.  Defaults to 'this'
419      * @param {Boolean} [config.forceReload] If true, the cache will be reloaded prior to performing the check
420      * @methodOf LABKEY.FileSystem.AbstractFileSystem#
421      */
422     checkForNameConflict: function(config)
423     {
424         var filename = this.concatPaths(config.path, this.getFileName(config.name));
425         config.scope = config.scope || this;
426 
427         this.listFiles({
428             path: config.path,
429             success: function (fs, path, records) {
430                 if (Ext.isFunction(config.success))
431                     config.success.defer(1, config.scope, [this, config.name, config.path, this.recordFromCache(filename)]);
432             },
433             failure: config.failure,
434             scope: this,
435             forceReload: config.forceReload
436         });
437     },
438 
439     /**
440      * Force reload on next listFiles call
441      * @ignore
442      * @param record
443      */
444     uncacheListing : function(record)
445     {
446         var path = (typeof record == "string") ? record : record.data.path;
447         this.directoryMap[path] = null;
448     },
449 
450     /**
451      * @ignore
452      * @param record
453      */
454     canRead : function(record)
455     {
456         return true;
457     },
458 
459     /**
460      * @ignore
461      * @param record
462      */
463     canWrite: function(record)
464     {
465         return true;
466     },
467 
468     /**
469      * @ignore
470      * @param record
471      */
472     canMkdir: function(record)
473     {
474         return true;
475     },
476 
477     /**
478      * @ignore
479      * @param record
480      */
481     canDelete : function(record)
482     {
483         return true;
484     },
485 
486     /**
487      * @ignore
488      * @param record
489      */
490     canMove : function(record)
491     {
492         return true;
493     },
494 
495     /**
496      * @ignore
497      * @param config
498      */
499     deletePath : function(config)   // callback(filesystem, success, path)
500     {
501         return false;
502     },
503 
504     /**
505      * @ignore
506      * @param config
507      */
508     createDirectory : function(config) // callback(filesystem, success, path)
509     {
510     },
511 
512     /**
513      * Called by listFiles(), return false on immediate fail
514      * @ignore
515      */
516     reloadFiles : function(config)
517     {
518         return false;
519     },
520 
521     /**
522      * @ignore
523      * @param config
524      */
525     getHistory : function(config) // callback(filesystem, success, path, history[])
526     {
527     },
528 
529     // protected
530 
531     _addFiles : function(path, records)
532     {
533         this.directoryMap[path] = records;
534         this.fireEvent(LABKEY.FileSystem.FILESYSTEM_EVENTS.fileschanged, this, path, records);
535     },
536 
537     /**
538      * For a supplied path, returns an array corresponding Ext Record from the cache
539      * @param {String} path The path of the directory
540      * @returns {Ext.Record[]} An array of Ext.Records representing the contents of the directory.  Returns null if the directory is not in the cache.
541      * @methodOf LABKEY.FileSystem.AbstractFileSystem#
542      * @name directoryFromCache
543      */
544     directoryFromCache : function(path)
545     {
546         var files = this.directoryMap[path];
547         if (!files && path && path.length>0 && path.charAt(path.length-1) == this.separator)
548             path = path.substring(0,path.length-1);
549         files = this.directoryMap[path];
550         return files;
551     },
552 
553     /**
554      * For a supplied path, returns the corresponding Ext Record from the cache
555      * @param {String} path The path of the file or directory
556      * @returns {Ext.Record} The Ext.Record for this file.  Returns null if the file is not found.
557      * @methodOf LABKEY.FileSystem.AbstractFileSystem#
558      * @name recordFromCache
559      */
560     recordFromCache : function(path)
561     {
562         if (!path || path == this.rootPath)
563             return this.rootRecord;
564         var parent = this.getParentPath(path) || this.rootPath;
565         var name = this.getFileName(path);
566         var files = this.directoryFromCache(parent);
567         if (!files)
568             return null;
569         for (var i=0 ; i<files.length ; i++)
570         {
571             var r = files[i];
572             if (r.data.name == name)
573                 return r;
574         }
575         return null;
576     },
577 
578     onReady : function(fn)
579     {
580         if (this.ready)
581             fn.call();
582         else
583             this.on(LABKEY.FileSystem.FILESYSTEM_EVENTS.ready, fn);
584     },
585 
586     // util
587 
588     /**
589      * A utility method to concatenate 2 strings into a normalized filepath
590      * @param {String} a The first path
591      * @param {String} b The first path
592      * @returns {String} The concatenated path
593      * @methodOf LABKEY.FileSystem.AbstractFileSystem#
594      */
595     concatPaths : function(a,b)
596     {
597         var c = 0;
598         if (a.length > 0 && a.charAt(a.length-1)==this.separator) c++;
599         if (b.length > 0 && b.charAt(0)==this.separator) c++;
600         if (c == 0)
601             return a + this.separator + b;
602         else if (c == 1)
603             return a + b;
604         else
605             return a + b.substring(1);
606     },
607 
608     /**
609      * A utility method to extract the parent path from a file or folder path
610      * @param {String} p The path to the file or directory
611      * @returns {String} The parent path
612      * @methodOf LABKEY.FileSystem.AbstractFileSystem#
613      */
614     getParentPath : function(p)
615     {
616         if (!p)
617             p = this.rootPath;
618         if (p.length > 1 && p.charAt(p.length-1) == this.separator)
619             p = p.substring(0,p.length-1);
620         var i = p.lastIndexOf(this.separator);
621         return i == -1 ? this.rootPath : p.substring(0,i+1);
622     },
623 
624     /**
625      * A utility method to extract the filename from a file path.
626      * @param {String} p The path to the file or directory
627      * @returns {String} The file name
628      * @methodOf LABKEY.FileSystem.AbstractFileSystem#
629      */
630     getFileName : function(p)
631     {
632         if (!p || p == this.rootPath)
633             return this.rootPath;
634         if (p.length > 1 && p.charAt(p.length-1) == this.separator)
635             p = p.substring(0,p.length-1);
636         var i = p.lastIndexOf(this.separator);
637         if (i > -1)
638             p = p.substring(i+1);
639         return p;
640     },
641 
642     /**
643      * A utility to test if a path is a direct child of another path
644      * @param {String} a The first path to test
645      * @param {String} b The second path to test
646      * @returns {Boolean} Returns true if the first path is a direct child of the second
647      * @methodOf LABKEY.FileSystem.AbstractFileSystem#
648      */
649     isChild: function(a, b){
650         return a.indexOf(b) == 0;
651         //return a.match(new RegExp('^' + b + '.+', 'i'));
652     }
653 
654 });
655 
656 
657 /**
658  * This class enables interaction with WebDav filesystems, such as the one exposed through LabKey Server.
659  * In addition to the properties and methods documented here, all methods from LABKEY.FileSystem.AbstractFileSystem are available.
660  * @class LABKEY.FileSystem.WebdavFileSystem
661  * @augments LABKEY.FileSystem.AbstractFileSystem
662  * @constructor
663  * @param config Configuration properties.
664  * @param {String} [config.containerPath] The path to the container to load (ie. '/home')
665  * @param {String} [config.filePath] The file path, relative to the containerPath (ie. '/mySubfolder'). Optional.
666  * @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.
667  * @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
668  * @param {string} [config.rootName] The display name for the root (ie. 'Fileset'). Optional.
669  * @param {array} [config.extraDataFields] An array of extra Ext.data.Field config objects that will be appended to the FileRecord. Optional.
670  * @example <script type="text/javascript">
671 
672     //this example loads files from: /_webdav/home/mySubfolder/@files
673     var fileSystem = new LABKEY.FileSystem.WebdavFileSystem({
674         containerPath: '/home',
675         filePath: '/mySubfolder'  //optional.
676     });
677 
678      //this would be identical to the example above
679      new LABKEY.FileSystem.WebdavFileSystem({
680          baseUrl: "/_webdav/home/mySubfolder/@files"
681      });
682 
683      //this creates the URL: /webdav/myProject/@pipeline
684      new LABKEY.FileSystem.WebdavFileSystem({
685          containerPath: '/myProject',
686          fileLink: '/@pipeline'
687      });
688 
689 
690     fileSystem.on('ready', function(fileSystem){
691         fileSystem.listFiles({
692             path: '/mySubfolder',
693             success: function(fileSystem, path, records){
694                 alert('It worked!');
695                 console.log(records);
696             },
697             scope: this
698         }, this);
699 
700         fileSystem.movePath({
701             source: '/myFile.xls',
702             destination: '/foo/myFile.xls',
703             isFile: true,
704             success: function(fileSystem, path, records){
705                 alert('It worked!');
706             },
707             failure: function(response, options){
708                 alert('It didnt work.  The error was ' + response.statusText);
709                 console.log(response);
710             },
711             scope: this
712         });
713     }, this);
714 
715 
716  </script>
717  */
718 LABKEY.FileSystem.WebdavFileSystem = function(config)
719 {
720     config = config || {};
721     Ext.apply(this, config, {
722         baseUrl: LABKEY.contextPath + "/_webdav",
723         rootPath: "/",
724         rootName : (LABKEY.serverName || "LabKey Server"),
725         fileLink: "/@files"
726     });
727     this.ready = false;
728     this.initialConfig = config;
729 
730     LABKEY.FileSystem.WebdavFileSystem.superclass.constructor.call(this);
731 
732     this.HistoryRecord = Ext.data.Record.create(['user', 'date', 'message', 'href']);
733     this.historyReader = new Ext.data.XmlReader({record : "entry"}, this.HistoryRecord);
734 
735     this.init(config);
736     this.reloadFile("/", (function()
737     {
738         this.ready = true;
739         this.fireEvent(LABKEY.FileSystem.FILESYSTEM_EVENTS.ready, this);
740     }).createDelegate(this));
741 };
742 
743 Ext.extend(LABKEY.FileSystem.WebdavFileSystem, LABKEY.FileSystem.AbstractFileSystem,
744 {
745     /**
746      * Returns the history for the file or directory at the supplied path
747      * @param config Configuration properties.
748      * @param {String} config.path Path to the file or directory
749      * @param {Function} config.success Success callback function.  It will be called with the following arguments:
750      * <li>Filesystem: A reference to the filesystem</li>
751      * <li>Path: The path that was loaded</li>
752      * <li>History: An array of records representing the history</li>
753      * @param {Function} [config.failure] Error callback function.  It will be called with the following arguments:
754      * <li>Response: the response object</li>
755      * @param {Object} [config.scope] The scope of the callback function
756      * @methodOf LABKEY.FileSystem.WebdavFileSystem#
757      */
758     getHistory : function(config)
759     {
760         config.scope = config.scope || this;
761         var body =  "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<propfind xmlns=\"DAV:\"><prop><history/></prop></propfind>";
762 
763         var proxy = new Ext.data.HttpProxy(
764         {
765             url: this.concatPaths(this.prefixUrl, config.path),
766             xmlData : body,
767             method: "PROPFIND",
768             headers: {"Depth" : "0"}
769         });
770         proxy.api.read.method = 'PROPFIND';
771 
772         var cb = function(response, args, success)
773         {
774             LABKEY.FileSystem.Util._processAjaxResponse(response);
775             if (success && Ext.isFunction(config.success))
776                 config.success.call(config.scope, args.filesystem, args.path, response.records);
777             else if (!success && Ext.isFunction(config.failure))
778                 config.failure.call(config.scope, response);
779         };
780         proxy.request('read', null, {method:"PROPFIND", depth:"0", propname : this.propNames}, this.historyReader, cb, this, {filesystem:this, path:config.path});
781     },
782 
783     /**
784      * Returns true if the current user can read the passed file
785      * In order to obtain the record for the desired file, recordFromCache() is normally used.
786      * @param {Ext.Record} record The Ext record associated with the file.  See LABKEY.AbstractFileSystem.FileRecord for more information.
787      * @methodOf LABKEY.FileSystem.WebdavFileSystem#
788      */
789     canRead : function(record)
790     {
791         var options = record.data.options;
792         return !options || -1 != options.indexOf('GET');
793     },
794 
795     /**
796      * Returns true if the current user can write to the passed file or location
797      * In order to obtain the record for the desired file, recordFromCache() is normally used.
798      * @param {Ext.Record} record The Ext record associated with the file.  See LABKEY.AbstractFileSystem.FileRecord for more information.
799      * @methodOf LABKEY.FileSystem.WebdavFileSystem#
800      */
801     canWrite : function(record)
802     {
803         var options = record.data.options;
804         return !options || -1 != options.indexOf("PUT");
805     },
806 
807     /**
808      * Returns true if the current user can create a folder in the passed location
809      * In order to obtain the record for the desired file, recordFromCache() is normally used.
810      * @param {Ext.Record} record The Ext record associated with the file.  See LABKEY.AbstractFileSystem.FileRecord for more information.
811      * @methodOf LABKEY.FileSystem.WebdavFileSystem#
812      */
813     canMkdir : function(record)
814     {
815         var options = record.data.options;
816         return !options || -1 != options.indexOf("MKCOL");
817     },
818 
819     /**
820      * Returns true if the current user can delete the passed file.
821      * In order to obtain the record for the desired file, recordFromCache() is normally used.
822      * @param {Ext.Record} record The Ext record associated with the file.  See LABKEY.AbstractFileSystem.FileRecord for more information.
823      * @methodOf LABKEY.FileSystem.WebdavFileSystem#
824      */
825     canDelete : function(record)
826     {
827         var options = record.data.options;
828         return !options || -1 != options.indexOf('DELETE');
829     },
830 
831     /**
832      * Returns true if the current user can move or rename the passed file
833      * In order to obtain the record for the desired file, recordFromCache() is normally used.
834      * @param {Ext.Record} record The Ext record associated with the file.  See LABKEY.AbstractFileSystem.FileRecord for more information.
835      * @methodOf LABKEY.FileSystem.WebdavFileSystem#
836      */
837     canMove : function(record)
838     {
839         var options = record.data.options;
840         return !options || -1 != options.indexOf('MOVE');
841     },
842 
843     /**
844      * Can be used to delete a file or folder.
845      * @param config Configuration properties.
846      * @param {String} config.path The source file, which should be a URL relative to the fileSystem's rootPath
847      * @param {Boolean} config.isFile Set to true is this represent a file, as opposed to a folder
848      * @param {Function} config.success Success callback function.  It will be called with the following arguments:
849      * <li>Filesystem: A reference to the filesystem</li>
850      * <li>Path: The path that was loaded</li>
851      * @param {Object} [config.failure] The error callback function.  It will be called with the following arguments:
852      * <li>Response: The XMLHttpRequest object containing the response data.</li>
853      * <li>Options: The parameter to the request call.</li>
854      * @param {Object} [config.scope] The scope of the callback functions
855      * @methodOf LABKEY.FileSystem.WebdavFileSystem#
856      */
857     deletePath : function(config)
858     {
859         config.scope = config.scope || this;
860         var resourcePath = this.concatPaths(this.prefixUrl, LABKEY.ActionURL.encodePath(config.path));
861         var fileSystem = this;
862         var connection = new Ext.data.Connection();
863 
864         connection.request({
865             method: "DELETE",
866             url: resourcePath,
867             scope: this,
868             success: function(response, options){
869                 var success = false;
870                 LABKEY.FileSystem.Util._processAjaxResponse(response);
871                 if (204 == response.status || 404 == response.status) // NO_CONTENT (success)
872                     success = true;
873                 else if (405 == response.status) // METHOD_NOT_ALLOWED
874                     success = false;
875 
876                 if (success)
877                 {
878                     fileSystem._deleteListing(config.path, config.isFile);
879 
880                     if (Ext.isFunction(config.success))
881                         config.success.call(config.scope, fileSystem, config.path);
882                 }
883                 else {
884                     if (Ext.isFunction(config.failure))
885                         config.failure.call(config.scope, response, options);
886                 }
887             },
888             failure: function(response, options)
889             {
890                 var success = false;
891                 LABKEY.FileSystem.Util._processAjaxResponse(response);
892                 if (response.status == 404)  //NOT_FOUND - not sure if this is the correct behavior or not
893                     success = true;
894 
895                 if (!success && Ext.isFunction(config.failure))
896                     config.failure.call(config.scope, response, options);
897                 if (success && Ext.isFunction(config.success))
898                     config.success.call(config.scope, fileSystem, config.path);
899             },
900             headers: {
901                 'Content-Type': 'application/json'
902             }
903         });
904 
905         return true;
906     },
907 
908     //private
909     _deleteListing: function(path, isFile)
910     {
911         var deleted = [];
912 
913         //always delete the record itself
914         var record = this.recordFromCache(path);
915         if (record) {
916             isFile = record.data.file;
917             deleted.push(record);
918             var parentPath = this.getParentPath(path);
919             var parentFolder = this.directoryMap[parentPath];
920             if (parentFolder){
921                 parentFolder.remove(record);
922             }
923         }
924 
925         // find all paths modified by this delete
926         if (!isFile)
927         {
928             var pathsRemoved = [];
929             for (var a in this.directoryMap)
930             {
931                 if (typeof a == 'string')
932                 {
933                     var idx = a.indexOf(path);
934                     if (idx == 0)
935                     {
936                         pathsRemoved.push(a);
937                         var r = this.recordFromCache(a);
938                         if (r)
939                             deleted.push(r);
940 
941                         parentPath = this.getParentPath(a);
942                         parentFolder = this.directoryMap[parentPath];
943                         if (parentFolder && r){
944                             parentFolder.remove(r);
945                         }
946                         if (this.directoryMap[a] && this.directoryMap[a].length)
947                             deleted = deleted.concat(this.directoryMap[a]);
948                     }
949                 }
950             }
951             this.uncacheListing(path);
952         }
953 
954         deleted = Ext.unique(deleted);
955         this.fireEvent(LABKEY.FileSystem.FILESYSTEM_EVENTS.filesremoved, this, path, deleted);
956     },
957 
958     /**
959      * Can be used to rename a file or folder.  This is simply a convenience wrapper for movePath().
960      * @param config Configuration properties.
961      * @param {String} config.source The source file, which should be relative to the fileSystem's rootPath
962      * @param {String} config.destination The target path, which should be the full path for the new file, relative to the fileSystem's rootPath
963      * @param {Boolean} config.isFile Set to true if the path is a file, as opposed to a directory
964      * @param {Function} config.success Success callback function.  It will be called with the following arguments:
965      * <li>Filesystem: A reference to the filesystem</li>
966      * <li>SourcePath: The path to the file/folder to be renamed</li>
967      * <li>DestPath: The new path for the renamed file/folder</li>
968      * @param {Object} [config.failure] The failure callback function.  Will be called with the following arguments:
969      * <li>Response: The XMLHttpRequest object containing the response data.</li>
970      * <li>Options: The parameter to the request call.</li>
971      * @param {Object} [config.scope] The scope of the callback function
972      * @param {Boolean} [config.overwrite] If true, files at the target location
973      * @methodOf LABKEY.FileSystem.WebdavFileSystem#
974 * @example <script type="text/javascript">
975     var fileSystem = new new LABKEY.FileSystem.WebdavFileSystem({
976         containerPath: '/home',
977         filePath: '/@files'  //optional.  this is the same as the default
978     });
979 
980     fileSystem.on('ready', function(fileSystem){
981         fileSystem.listFiles({
982             path: '/mySubfolder/',
983             success: function(fileSystem, path, records){
984                 alert('It worked!');
985                 console.log(records);
986             },
987             scope: this
988         }, this);
989 
990         fileSystem.renamePath({
991             source: 'myFile.xls',
992             destination: 'renamedFile.xls',
993             isFile: true,
994             scope: this
995         });
996 
997 
998         //if you renamed a file in a subfolder, you can optionally supply the fileName only
999         //this file will be renamed to: '/subfolder/renamedFile.xls'
1000         fileSystem.renamePath({
1001             source: '/subfolder/myFile.xls',
1002             destination: 'renamedFile.xls',
1003             isFile: true,
1004             scope: this
1005         });
1006 
1007         //or provide the entire path
1008         fileSystem.renamePath({
1009             source: '/subfolder/myFile.xls',
1010             destination: '/subfolder/renamedFile.xls',
1011             isFile: true,
1012             scope: this
1013         });
1014     }, this);
1015 
1016 
1017 </script>
1018      */
1019     renamePath : function(config)
1020     {
1021         //allow user to submit either full path for rename, or just the new filename
1022         if (config.source.indexOf(this.separator) > -1 && config.destination.indexOf(this.separator) == -1){
1023             config.destination = this.concatPaths(this.getParentPath(config.source), config.destination);
1024         }
1025 
1026         this.movePath({
1027             source: config.source,
1028             destination: config.destination,
1029             isFile: config.isFile,
1030             success: config.success,
1031             failure: config.failure,
1032             scope: config.scope,
1033             overwrite: config.overwrite
1034         });
1035     },
1036 
1037     /**
1038      * Can be used to move a file or folder from one location to another.
1039      * @param config Configuration properties.
1040      * @param {String} config.path The source file, which should be a URL relative to the fileSystem's rootPath
1041      * @param {String} config.destination The target path, which should be a URL relative to the fileSystem's rootPath
1042      * @param {Boolean} config.isFile True if the file to move is a file, as opposed to a directory
1043      * @param {Function} config.success Success callback function.  It will be called with the following arguments:
1044      * <li>Filesystem: A reference to the filesystem</li>
1045      * <li>SourcePath: The path that was loaded</li>
1046      * <li>DestPath: The path that was loaded</li>
1047      * @param {Object} [config.failure] The failure callback function.  Will be called with the following arguments:
1048      * <li>Response: The XMLHttpRequest object containing the response data.</li>
1049      * <li>Options: The parameter to the request call.</li>
1050      * @param {Object} [config.scope] The scope of the callbacks
1051      * @param {Boolean} [config.overwrite] If true, files at the target location
1052      * @methodOf LABKEY.FileSystem.WebdavFileSystem#
1053      */
1054     movePath : function(config)
1055     {
1056         config.scope  = config.scope || this;
1057 
1058         var resourcePath = this.concatPaths(this.prefixUrl, LABKEY.ActionURL.encodePath(config.source));
1059         var destinationPath = this.concatPaths(this.prefixUrl, LABKEY.ActionURL.encodePath(config.destination));
1060         var fileSystem = this;
1061         var connection = new Ext.data.Connection();
1062 
1063         var cfg = {
1064             method: "MOVE",
1065             url: resourcePath,
1066             scope: this,
1067             failure: function(response){
1068                 LABKEY.FileSystem.Util._processAjaxResponse(response);
1069                 if(Ext.isFunction(config.failure))
1070                     config.failure.apply(config.scope, arguments);
1071             },
1072             success: function(response, options){
1073                 LABKEY.FileSystem.Util._processAjaxResponse(response);
1074                 var success = false;
1075                 if (201 == response.status || 204 == response.status) //CREATED,  NO_CONTENT (success)
1076                     success = true;
1077                 else
1078                     success = false;
1079 
1080                 if (success)
1081                 {
1082                     //the move is performed as a delete / lazy-insert
1083                     fileSystem._deleteListing(config.source, config.isFile);
1084 
1085                     var destParent = fileSystem.getParentPath(config.destination);
1086                     fileSystem.uncacheListing(destParent); //this will cover uncaching children too
1087 
1088                     // TODO: maybe support a config option that will to force the fileSystem to
1089                     // auto-reload this location, instead just uncaching and relying on consumers to do it??
1090                     this.fireEvent(LABKEY.FileSystem.FILESYSTEM_EVENTS.fileschanged, this, destParent);
1091 
1092                     if (Ext.isFunction(config.success))
1093                         config.success.call(config.scope, fileSystem, config.source, config.destination);
1094                 }
1095                 else {
1096                     if (Ext.isFunction(config.failure))
1097                         config.failure.call(config.scope, response, options);
1098                 }
1099             },
1100             headers: {
1101                 Destination: destinationPath,
1102                 'Content-Type': 'application/json'
1103             }
1104         };
1105 
1106         if (config.overwrite)
1107             cfg.headers.Overwrite = 'T';
1108 
1109         connection.request(cfg);
1110 
1111         return true;
1112     },
1113 
1114     /**
1115      * Will create a directory at the provided location.  This does not perform permission checking, which can be done using canMkDir().
1116      * @param config Configuration properties.
1117      * @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.
1118      * @param {Function} config.success Success callback function.  It will be called with the following arguments:
1119      * <li>Filesystem: A reference to the filesystem</li>
1120      * <li>Path: The path that was created</li>
1121      * @param {Object} [config.failure] Failure callback function.  It will be called with the following arguments:
1122      * <li>Response: the response object</li>
1123      * <li>Options: The parameter to the request call.</li>
1124      * @param {Object} [config.scope] The scope of the callback functions.
1125      * @methodOf LABKEY.FileSystem.WebdavFileSystem#
1126      */
1127     createDirectory : function(config)
1128     {
1129         var fileSystem = this;
1130         config.scope = config.scope || this;
1131 
1132         var resourcePath = this.concatPaths(this.prefixUrl, config.path);
1133         var connection = new Ext.data.Connection();
1134 
1135         connection.request({
1136             method: "MKCOL",
1137             url: resourcePath,
1138             scope: this,
1139             success: function(response, options){
1140                 LABKEY.FileSystem.Util._processAjaxResponse(response);
1141                 var success = false;
1142                 if (200 == response.status || 201 == response.status){   // OK, CREATED
1143                     if(!response.responseText)
1144                         success = true;
1145                     else
1146                         success = false;
1147                 }
1148                 else if (405 == response.status) // METHOD_NOT_ALLOWED
1149                     success = false;
1150 
1151                 if (success && Ext.isFunction(config.success))
1152                     config.success.call(config.scope, fileSystem, config.path);
1153                 if (!success && Ext.isFunction(config.failure))
1154                     config.failure.call(config.scope, response, options);
1155             },
1156             failure: function(response){
1157                 LABKEY.FileSystem.Util._processAjaxResponse(response);
1158                 if (Ext.isFunction(config.failure))
1159                     config.failure.apply(config.scope, arguments);
1160             },
1161             headers: {
1162                 'Content-Type': 'application/json'
1163             }
1164         });
1165 
1166         return true;
1167     },
1168 
1169     //private
1170     // not sure why both this and reloadFiles() exist?  reloadFile() seems to be used internally only
1171     reloadFile : function(path, callback)
1172     {
1173         var url = this.concatPaths(this.prefixUrl, LABKEY.ActionURL.encodePath(path));
1174         this.connection.url = url;
1175         var args = {url: url, path: path, callback:callback};
1176         this.proxy.doRequest("read", null, {method:"PROPFIND",depth:"0", propname : this.propNames}, this.transferReader, this.processFile, this, args);
1177         return true;
1178     },
1179 
1180     //private
1181     _updateRecord : function(update)
1182     {
1183         var path = update.data.path;
1184         if (path == '/')
1185         {
1186             Ext.apply(this.rootRecord.data, update.data);
1187         }
1188         else
1189         {
1190             var record = this.recordFromCache(path);
1191             if (record)
1192                 Ext.apply(record.data, update.data);
1193         }
1194     },
1195 
1196     //private
1197     processFile : function(result, args, success)
1198     {
1199         var update = null;
1200         if (success && result && !Ext.isArray(result.records))
1201             success = false;
1202         if (success && result.records.length == 1)
1203         {
1204             update = result.records[0];
1205             this._updateRecord(update);
1206         }
1207 
1208         if (Ext.isFunction(args.callback))
1209             args.callback(this, success && null != update, args.path, update);
1210     },
1211 
1212     //private
1213     uncacheListing : function(record)
1214     {
1215         var path = (typeof record == "string") ? record : record.data.path;
1216 
1217         // want to uncache all subfolders of the parent folder
1218         Ext.iterate(this.directoryMap, function(key, value) {
1219             if (Ext.isString(key)) {
1220                 if (key.indexOf(path) === 0) {
1221                     this.directoryMap[key] = null;
1222                 }
1223             }
1224         }, this);
1225 
1226         var args = this.pendingPropfind[path];
1227         if (args && args.transId)
1228         {
1229             this.connection.abort(args.transId);
1230             this.connection.url = args.url;
1231             this.proxy.doRequest("read", null, {method:"PROPFIND",depth:"1", propname : this.propNames}, this.transferReader, this.processFiles, this, args);
1232             args.transId = this.connection.transId;
1233         }
1234     },
1235 
1236     //private
1237     reloadFiles : function(config)
1238     {
1239         config.scope = config.scope || this;
1240 
1241         var cb = {
1242             success: config.success,
1243             failure: config.failure,
1244             scope: config.scope
1245         };
1246 
1247         var args = this.pendingPropfind[config.path];
1248         if (args)
1249         {
1250             args.callbacks.push(cb);
1251             return;
1252         }
1253 
1254         var url = this.concatPaths(this.prefixUrl, LABKEY.ActionURL.encodePath(config.path));
1255         this.connection.url = url;
1256         this.pendingPropfind[config.path] = args = {url: url, path: config.path, callbacks:[cb]};
1257         this.proxy.doRequest("read", null, {method:"PROPFIND",depth:"1", propname : this.propNames}, this.transferReader, this.processFiles, this, args);
1258         args.transId = this.connection.transId;
1259         return true;
1260     },
1261 
1262     //private
1263     processFiles : function(result, args, success)
1264     {
1265         delete this.pendingPropfind[args.path];
1266 
1267         var path = args.path;
1268 
1269         var directory = null;
1270         var listing = [];
1271         if (success && result && !Ext.isArray(result.records))
1272             success = false;
1273         if (success)
1274         {
1275             var records = result.records;
1276             for (var r=0 ; r<records.length ; r++)
1277             {
1278                 var record = records[r];
1279                 if (record.data.path == path)
1280                     directory = record;
1281                 else
1282                     listing.push(record);
1283             }
1284             if (directory)
1285                 this._updateRecord(directory);
1286             this._addFiles(path, listing);
1287         }
1288 
1289         var callbacks = args.callbacks;
1290         for (var i=0 ; i<callbacks.length ; i++)
1291         {
1292             var callback = callbacks[i];
1293             if (Ext.isFunction(callback)) {
1294                 callback(this, path, listing);
1295             }
1296             else if (typeof callback == 'object') {
1297                 var scope = callback.scope || this;
1298                 if (success && Ext.isFunction(callback.success))
1299                     callback.success.call(scope, this, path, listing);
1300                 else if (!success && Ext.isFunction(callback.failure))
1301                     callback.failure.call(scope, args.transId.conn);
1302             }
1303         }
1304     },
1305 
1306     //private
1307     init : function(config)
1308     {
1309         //support either containerPath + path OR baseUrl (which is the concatenation of these 2)
1310         this.filePath = this.filePath || '';
1311         this.fileLink = this.fileLink || '';
1312         this.containerPath = this.containerPath || LABKEY.ActionURL.getContainer();
1313 
1314         if (!config.baseUrl){
1315             this.baseUrl = this.concatPaths(LABKEY.contextPath + "/_webdav", encodeURI(this.containerPath));
1316             this.baseUrl = this.concatPaths(this.baseUrl, encodeURI(this.filePath));
1317             this.baseUrl = this.concatPaths(this.baseUrl, encodeURI(this.fileLink));
1318         }
1319 
1320         var prefix = this.concatPaths(this.baseUrl, this.rootPath);
1321         if (prefix.length > 0 && prefix.charAt(prefix.length-1) == this.separator)
1322             prefix = prefix.substring(0,prefix.length-1);
1323         this.prefixUrl = prefix;
1324         this.pendingPropfind = {};
1325 
1326         var prefixDecode  = decodeURIComponent(prefix);
1327 
1328         var getURI = function(v,rec)
1329         {
1330             var uri = rec.uriOBJECT || new LABKEY.URI(v);
1331             if (!Ext.isIE && !rec.uriOBJECT)
1332                 try {rec.uriOBJECT = uri;} catch (e) {};
1333             return uri;
1334         };
1335 
1336         this.propNames = ["creationdate", "displayname", "createdby", "getlastmodified", "modifiedby", "getcontentlength",
1337                      "getcontenttype", "getetag", "resourcetype", "source", "path", "iconHref", "options", "absolutePath"];
1338 
1339         if (config.extraPropNames && config.extraPropNames.length)
1340             this.propNames = this.propNames.concat(config.extraPropNames);
1341 
1342         var recordCfg = [
1343             {name: 'uri', mapping: 'href',
1344                 convert : function(v, rec)
1345                 {
1346                     var uri = getURI(v,rec);
1347                     return uri ? uri.href : "";
1348                 }
1349             },
1350             {name: 'fileLink', mapping: 'href',
1351                 convert : function(v, rec)
1352                 {
1353                     var uri = getURI(v,rec);
1354 
1355                     if (uri && uri.file)
1356                         return Ext.DomHelper.markup({
1357                             tag  :'a',
1358                             href : Ext.util.Format.htmlEncode(uri.href + '?contentDisposition=attachment'),
1359                             html : Ext.util.Format.htmlEncode(decodeURIComponent(uri.file))});
1360                     else
1361                         return '';
1362                 }
1363             },
1364             {name: 'path', mapping: 'href',
1365                 convert : function (v, rec)
1366                 {
1367                     var uri = getURI(v,rec);
1368                     var path = decodeURIComponent(uri.pathname);
1369                     if (path.length >= prefixDecode.length && path.substring(0,prefixDecode.length) == prefixDecode)
1370                         path = path.substring(prefixDecode.length);
1371                     return path;
1372                 }
1373             },
1374             {name: 'name', mapping: 'propstat/prop/displayname', sortType:'asUCString'},
1375             {name: 'fileExt', mapping: 'propstat/prop/displayname',
1376                 convert : function (v, rec)
1377                 {
1378                     // parse the file extension from the file name
1379                     var idx = v.lastIndexOf('.');
1380                     if (idx != -1)
1381                         return v.substring(idx+1);
1382                     return '';
1383                 }
1384             },
1385 
1386             {name: 'file', mapping: 'href', type: 'boolean',
1387                 convert : function (v, rec)
1388                 {
1389                     // UNDONE: look for <collection>
1390                     var uri = getURI(v, rec);
1391                     var path = uri.pathname;
1392                     return path.length > 0 && path.charAt(path.length-1) != '/';
1393                 }
1394             },
1395             {name: 'created', mapping: 'propstat/prop/creationdate', type: 'date', dateFormat : "c"},
1396             {name: 'createdBy', mapping: 'propstat/prop/createdby'},
1397             {name: 'modified', mapping: 'propstat/prop/getlastmodified', type: 'date'},
1398             {name: 'modifiedBy', mapping: 'propstat/prop/modifiedby'},
1399             {name: 'size', mapping: 'propstat/prop/getcontentlength', type: 'int'},
1400             {name: 'absolutePath', mapping: 'propstat/prop/absolutePath', type: 'string'},
1401             {name: 'iconHref'},
1402             {name: 'contentType', mapping: 'propstat/prop/getcontenttype'},
1403             {name: 'options'}
1404         ];
1405 
1406         if (config.extraDataFields && config.extraDataFields.length)
1407             recordCfg = recordCfg.concat(config.extraDataFields);
1408 
1409         this.FileRecord = Ext.data.Record.create(recordCfg);
1410         this.connection = new Ext.data.Connection({method: "GET", timeout: 600000, headers: {"Method" : "PROPFIND", "Depth" : "1", propname : this.propNames}});
1411         this.proxy = new Ext.data.HttpProxy(this.connection);
1412         this.transferReader = new Ext.data.XmlReader({record : "response", id : "href"}, this.FileRecord);
1413 
1414         this.rootRecord = new this.FileRecord({
1415             id:"/",
1416             path:"/",
1417             name: this.rootName,
1418             file:false,
1419             uri:this.prefixUrl,
1420             iconHref: LABKEY.contextPath + "/_images/labkey.png"
1421         }, "/");
1422     }
1423 });
1424 
1425 
1426 
1427