1/*
2 * Copyright (C) 2013 Google Inc. All rights reserved.
3 *
4 * Redistribution and use in source and binary forms, with or without
5 * modification, are permitted provided that the following conditions are
6 * met:
7 *
8 *     * Redistributions of source code must retain the above copyright
9 * notice, this list of conditions and the following disclaimer.
10 *     * Redistributions in binary form must reproduce the above
11 * copyright notice, this list of conditions and the following disclaimer
12 * in the documentation and/or other materials provided with the
13 * distribution.
14 *     * Neither the name of Google Inc. nor the names of its
15 * contributors may be used to endorse or promote products derived from
16 * this software without specific prior written permission.
17 *
18 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
19 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
20 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
21 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
22 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
23 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
24 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
25 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
26 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
27 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29 */
30
31/**
32 * @constructor
33 * @param {!WebInspector.IsolatedFileSystemManager} manager
34 * @param {string} path
35 * @param {string} name
36 * @param {string} rootURL
37 */
38WebInspector.IsolatedFileSystem = function(manager, path, name, rootURL)
39{
40    this._manager = manager;
41    this._path = path;
42    this._name = name;
43    this._rootURL = rootURL;
44}
45
46/**
47 * @param {!FileError} error
48 * @return {string}
49 */
50WebInspector.IsolatedFileSystem.errorMessage = function(error)
51{
52    return WebInspector.UIString("File system error: %s", error.message);
53}
54
55/**
56 * @param {string} fileSystemPath
57 * @return {string}
58 */
59WebInspector.IsolatedFileSystem.normalizePath = function(fileSystemPath)
60{
61    if (WebInspector.isWin())
62        return fileSystemPath.replace(/\\/g, "/");
63    return fileSystemPath;
64}
65
66WebInspector.IsolatedFileSystem.prototype = {
67    /**
68     * @return {string}
69     */
70    path: function()
71    {
72        return this._path;
73    },
74
75    /**
76     * @return {string}
77     */
78    normalizedPath: function()
79    {
80        if (this._normalizedPath)
81            return this._normalizedPath;
82        this._normalizedPath = WebInspector.IsolatedFileSystem.normalizePath(this._path);
83        return this._normalizedPath;
84    },
85
86    /**
87     * @return {string}
88     */
89    name: function()
90    {
91        return this._name;
92    },
93
94    /**
95     * @return {string}
96     */
97    rootURL: function()
98    {
99        return this._rootURL;
100    },
101
102    /**
103     * @param {function(?DOMFileSystem)} callback
104     */
105    _requestFileSystem: function(callback)
106    {
107        this._manager.requestDOMFileSystem(this._path, callback);
108    },
109
110    /**
111     * @param {string} path
112     * @param {function(string)} fileCallback
113     * @param {function()=} finishedCallback
114     */
115    requestFilesRecursive: function(path, fileCallback, finishedCallback)
116    {
117        var domFileSystem;
118        var pendingRequests = 0;
119        this._requestFileSystem(fileSystemLoaded.bind(this));
120        /**
121         * @param {?DOMFileSystem} fs
122         * @this {WebInspector.IsolatedFileSystem}
123         */
124        function fileSystemLoaded(fs)
125        {
126            domFileSystem = /** @type {!DOMFileSystem} */ (fs);
127            console.assert(domFileSystem);
128            ++pendingRequests;
129            this._requestEntries(domFileSystem, path, innerCallback.bind(this));
130        }
131
132        /**
133         * @param {!Array.<!FileEntry>} entries
134         * @this {WebInspector.IsolatedFileSystem}
135         */
136        function innerCallback(entries)
137        {
138            for (var i = 0; i < entries.length; ++i) {
139                var entry = entries[i];
140                if (!entry.isDirectory) {
141                    if (this._manager.mapping().isFileExcluded(this._path, entry.fullPath))
142                        continue;
143                    fileCallback(entry.fullPath.substr(1));
144                }
145                else {
146                    if (this._manager.mapping().isFileExcluded(this._path, entry.fullPath + "/"))
147                        continue;
148                    ++pendingRequests;
149                    this._requestEntries(domFileSystem, entry.fullPath, innerCallback.bind(this));
150                }
151            }
152            if (finishedCallback && (--pendingRequests === 0))
153                finishedCallback();
154        }
155    },
156
157    /**
158     * @param {string} path
159     * @param {?string} name
160     * @param {function(?string)} callback
161     */
162    createFile: function(path, name, callback)
163    {
164        this._requestFileSystem(fileSystemLoaded.bind(this));
165        var newFileIndex = 1;
166        if (!name)
167            name = "NewFile";
168        var nameCandidate;
169
170        /**
171         * @param {?DOMFileSystem} fs
172         * @this {WebInspector.IsolatedFileSystem}
173         */
174        function fileSystemLoaded(fs)
175        {
176            var domFileSystem = /** @type {!DOMFileSystem} */ (fs);
177            console.assert(domFileSystem);
178            domFileSystem.root.getDirectory(path, null, dirEntryLoaded.bind(this), errorHandler.bind(this));
179        }
180
181        /**
182         * @param {!DirectoryEntry} dirEntry
183         * @this {WebInspector.IsolatedFileSystem}
184         */
185        function dirEntryLoaded(dirEntry)
186        {
187            var nameCandidate = name;
188            if (newFileIndex > 1)
189                nameCandidate += newFileIndex;
190            ++newFileIndex;
191            dirEntry.getFile(nameCandidate, { create: true, exclusive: true }, fileCreated, fileCreationError.bind(this));
192
193            function fileCreated(entry)
194            {
195                callback(entry.fullPath.substr(1));
196            }
197
198            /**
199             * @this {WebInspector.IsolatedFileSystem}
200             */
201            function fileCreationError(error)
202            {
203                if (error.code === FileError.INVALID_MODIFICATION_ERR) {
204                    dirEntryLoaded.call(this, dirEntry);
205                    return;
206                }
207
208                var errorMessage = WebInspector.IsolatedFileSystem.errorMessage(error);
209                console.error(errorMessage + " when testing if file exists '" + (this._path + "/" + path + "/" + nameCandidate) + "'");
210                callback(null);
211            }
212        }
213
214        /**
215         * @this {WebInspector.IsolatedFileSystem}
216         */
217        function errorHandler(error)
218        {
219            var errorMessage = WebInspector.IsolatedFileSystem.errorMessage(error);
220            var filePath = this._path + "/" + path;
221            if (nameCandidate)
222                filePath += "/" + nameCandidate;
223            console.error(errorMessage + " when getting content for file '" + (filePath) + "'");
224            callback(null);
225        }
226    },
227
228    /**
229     * @param {string} path
230     */
231    deleteFile: function(path)
232    {
233        this._requestFileSystem(fileSystemLoaded.bind(this));
234
235        /**
236         * @param {?DOMFileSystem} fs
237         * @this {WebInspector.IsolatedFileSystem}
238         */
239        function fileSystemLoaded(fs)
240        {
241            var domFileSystem = /** @type {!DOMFileSystem} */ (fs);
242            console.assert(domFileSystem);
243            domFileSystem.root.getFile(path, null, fileEntryLoaded.bind(this), errorHandler.bind(this));
244        }
245
246        /**
247         * @param {!FileEntry} fileEntry
248         * @this {WebInspector.IsolatedFileSystem}
249         */
250        function fileEntryLoaded(fileEntry)
251        {
252            fileEntry.remove(fileEntryRemoved, errorHandler.bind(this));
253        }
254
255        function fileEntryRemoved()
256        {
257        }
258
259        /**
260         * @param {!FileError} error
261         * @this {WebInspector.IsolatedFileSystem}
262         */
263        function errorHandler(error)
264        {
265            var errorMessage = WebInspector.IsolatedFileSystem.errorMessage(error);
266            console.error(errorMessage + " when deleting file '" + (this._path + "/" + path) + "'");
267        }
268    },
269
270    /**
271     * @param {string} path
272     * @param {function(?Date, ?number)} callback
273     */
274    requestMetadata: function(path, callback)
275    {
276        this._requestFileSystem(fileSystemLoaded);
277
278        /**
279         * @param {?DOMFileSystem} fs
280         */
281        function fileSystemLoaded(fs)
282        {
283            var domFileSystem = /** @type {!DOMFileSystem} */ (fs);
284            console.assert(domFileSystem);
285            domFileSystem.root.getFile(path, null, fileEntryLoaded, errorHandler);
286        }
287
288        /**
289         * @param {!FileEntry} entry
290         */
291        function fileEntryLoaded(entry)
292        {
293            entry.getMetadata(successHandler, errorHandler);
294        }
295
296        /**
297         * @param {!Metadata} metadata
298         */
299        function successHandler(metadata)
300        {
301            callback(metadata.modificationTime, metadata.size);
302        }
303
304        /**
305         * @param {!FileError} error
306         */
307        function errorHandler(error)
308        {
309            callback(null, null);
310        }
311    },
312
313    /**
314     * @param {string} path
315     * @param {function(?string)} callback
316     */
317    requestFileContent: function(path, callback)
318    {
319        this._requestFileSystem(fileSystemLoaded.bind(this));
320
321        /**
322         * @param {?DOMFileSystem} fs
323         * @this {WebInspector.IsolatedFileSystem}
324         */
325        function fileSystemLoaded(fs)
326        {
327            var domFileSystem = /** @type {!DOMFileSystem} */ (fs);
328            console.assert(domFileSystem);
329            domFileSystem.root.getFile(path, null, fileEntryLoaded.bind(this), errorHandler.bind(this));
330        }
331
332        /**
333         * @param {!FileEntry} entry
334         * @this {WebInspector.IsolatedFileSystem}
335         */
336        function fileEntryLoaded(entry)
337        {
338            entry.file(fileLoaded, errorHandler.bind(this));
339        }
340
341        /**
342         * @param {!Blob} file
343         */
344        function fileLoaded(file)
345        {
346            var reader = new FileReader();
347            reader.onloadend = readerLoadEnd;
348            reader.readAsText(file);
349        }
350
351        /**
352         * @this {!FileReader}
353         */
354        function readerLoadEnd()
355        {
356            callback(/** @type {string} */ (this.result));
357        }
358
359        /**
360         * @this {WebInspector.IsolatedFileSystem}
361         */
362        function errorHandler(error)
363        {
364            if (error.code === FileError.NOT_FOUND_ERR) {
365                callback(null);
366                return;
367            }
368
369            var errorMessage = WebInspector.IsolatedFileSystem.errorMessage(error);
370            console.error(errorMessage + " when getting content for file '" + (this._path + "/" + path) + "'");
371            callback(null);
372        }
373    },
374
375    /**
376     * @param {string} path
377     * @param {string} content
378     * @param {function()} callback
379     */
380    setFileContent: function(path, content, callback)
381    {
382        this._requestFileSystem(fileSystemLoaded.bind(this));
383        WebInspector.userMetrics.FileSavedInWorkspace.record();
384
385        /**
386         * @param {?DOMFileSystem} fs
387         * @this {WebInspector.IsolatedFileSystem}
388         */
389        function fileSystemLoaded(fs)
390        {
391            var domFileSystem = /** @type {!DOMFileSystem} */ (fs);
392            console.assert(domFileSystem);
393            domFileSystem.root.getFile(path, { create: true }, fileEntryLoaded.bind(this), errorHandler.bind(this));
394        }
395
396        /**
397         * @param {!FileEntry} entry
398         * @this {WebInspector.IsolatedFileSystem}
399         */
400        function fileEntryLoaded(entry)
401        {
402            entry.createWriter(fileWriterCreated.bind(this), errorHandler.bind(this));
403        }
404
405        /**
406         * @param {!FileWriter} fileWriter
407         * @this {WebInspector.IsolatedFileSystem}
408         */
409        function fileWriterCreated(fileWriter)
410        {
411            fileWriter.onerror = errorHandler.bind(this);
412            fileWriter.onwriteend = fileTruncated;
413            fileWriter.truncate(0);
414
415            function fileTruncated()
416            {
417                fileWriter.onwriteend = writerEnd;
418                var blob = new Blob([content], { type: "text/plain" });
419                fileWriter.write(blob);
420            }
421        }
422
423        function writerEnd()
424        {
425            callback();
426        }
427
428        /**
429         * @this {WebInspector.IsolatedFileSystem}
430         */
431        function errorHandler(error)
432        {
433            var errorMessage = WebInspector.IsolatedFileSystem.errorMessage(error);
434            console.error(errorMessage + " when setting content for file '" + (this._path + "/" + path) + "'");
435            callback();
436        }
437    },
438
439    /**
440     * @param {string} path
441     * @param {string} newName
442     * @param {function(boolean, string=)} callback
443     */
444    renameFile: function(path, newName, callback)
445    {
446        newName = newName ? newName.trim() : newName;
447        if (!newName || newName.indexOf("/") !== -1) {
448            callback(false);
449            return;
450        }
451        var fileEntry;
452        var dirEntry;
453        var newFileEntry;
454        this._requestFileSystem(fileSystemLoaded.bind(this));
455
456        /**
457         * @param {?DOMFileSystem} fs
458         * @this {WebInspector.IsolatedFileSystem}
459         */
460        function fileSystemLoaded(fs)
461        {
462            var domFileSystem = /** @type {!DOMFileSystem} */ (fs);
463            console.assert(domFileSystem);
464            domFileSystem.root.getFile(path, null, fileEntryLoaded.bind(this), errorHandler.bind(this));
465        }
466
467        /**
468         * @param {!FileEntry} entry
469         * @this {WebInspector.IsolatedFileSystem}
470         */
471        function fileEntryLoaded(entry)
472        {
473            if (entry.name === newName) {
474                callback(false);
475                return;
476            }
477
478            fileEntry = entry;
479            fileEntry.getParent(dirEntryLoaded.bind(this), errorHandler.bind(this));
480        }
481
482        /**
483         * @param {!Entry} entry
484         * @this {WebInspector.IsolatedFileSystem}
485         */
486        function dirEntryLoaded(entry)
487        {
488            dirEntry = entry;
489            dirEntry.getFile(newName, null, newFileEntryLoaded, newFileEntryLoadErrorHandler.bind(this));
490        }
491
492        /**
493         * @param {!FileEntry} entry
494         */
495        function newFileEntryLoaded(entry)
496        {
497            callback(false);
498        }
499
500        /**
501         * @this {WebInspector.IsolatedFileSystem}
502         */
503        function newFileEntryLoadErrorHandler(error)
504        {
505            if (error.code !== FileError.NOT_FOUND_ERR) {
506                callback(false);
507                return;
508            }
509            fileEntry.moveTo(dirEntry, newName, fileRenamed, errorHandler.bind(this));
510        }
511
512        /**
513         * @param {!FileEntry} entry
514         */
515        function fileRenamed(entry)
516        {
517            callback(true, entry.name);
518        }
519
520        /**
521         * @this {WebInspector.IsolatedFileSystem}
522         */
523        function errorHandler(error)
524        {
525            var errorMessage = WebInspector.IsolatedFileSystem.errorMessage(error);
526            console.error(errorMessage + " when renaming file '" + (this._path + "/" + path) + "' to '" + newName + "'");
527            callback(false);
528        }
529    },
530
531    /**
532     * @param {!DirectoryEntry} dirEntry
533     * @param {function(!Array.<!FileEntry>)} callback
534     */
535    _readDirectory: function(dirEntry, callback)
536    {
537        var dirReader = dirEntry.createReader();
538        var entries = [];
539
540        function innerCallback(results)
541        {
542            if (!results.length)
543                callback(entries.sort());
544            else {
545                entries = entries.concat(toArray(results));
546                dirReader.readEntries(innerCallback, errorHandler);
547            }
548        }
549
550        function toArray(list)
551        {
552            return Array.prototype.slice.call(list || [], 0);
553        }
554
555        dirReader.readEntries(innerCallback, errorHandler);
556
557        function errorHandler(error)
558        {
559            var errorMessage = WebInspector.IsolatedFileSystem.errorMessage(error);
560            console.error(errorMessage + " when reading directory '" + dirEntry.fullPath + "'");
561            callback([]);
562        }
563    },
564
565    /**
566     * @param {!DOMFileSystem} domFileSystem
567     * @param {string} path
568     * @param {function(!Array.<!FileEntry>)} callback
569     */
570    _requestEntries: function(domFileSystem, path, callback)
571    {
572        domFileSystem.root.getDirectory(path, null, innerCallback.bind(this), errorHandler);
573
574        /**
575         * @param {!DirectoryEntry} dirEntry
576         * @this {WebInspector.IsolatedFileSystem}
577         */
578        function innerCallback(dirEntry)
579        {
580            this._readDirectory(dirEntry, callback)
581        }
582
583        function errorHandler(error)
584        {
585            var errorMessage = WebInspector.IsolatedFileSystem.errorMessage(error);
586            console.error(errorMessage + " when requesting entry '" + path + "'");
587            callback([]);
588        }
589    }
590}
591