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