5
\$\begingroup\$

I am writing a file writer/reader in JavaScript; I want it to work on local files. It tries to use Firefox's XpConnect, falls back on IE activeX, falls back on Java LiveConnect, falls back on a Java class (not included here), falls back on HTML5 local filestorage. I also want to throw in there a GET driver, to communicate with PHP/whatever and be able to use the same API for a web server file browser.

I have never developed in JS and I am having trouble with lexical scoping and inheritance. The code I am presenting is very far from complete, but I am presenting it now because it is not too complex yet, to see if I am on the right track.

I am not asking for a detailed review (yet), I just want to know if I am heading in the right direction before advancing too much.

In order to help read the code, here is the loop:

You create a new jsio object. Then you can use jsio.save(fileName,content) or jsio.load(fileName). You can specify a driver through jsio.driver(driverInstance) OR let it detect the driver itself (following the fallback loop described above).
the TestCapabilities() is there to help determine the right driver.

(function(context){

    var javaAppletName = 'myIOJavaApplet';

    var testCapabilities = {
        xpConnect: function(){
            if(window.Components){
                try {
                    netscape.security.PrivilegeManager.enablePrivilege("UniversalXPConnect");
                    return true;
                }catch(err){}
            }
            return false;
        },
        activeX: function(){
            try {
                    new ActiveXObject("Scripting.FileSystemObject");
                    return true;
                } catch(err) {}
            return false;
        },
        java: function(){
            if(document.applets[javaAppletName] || java.io){return true;}
            return false;
        },
        local:function(){
            if(window.requestFileSystem || window.webkitRequestFileSystem){return true;};
            return false;
        }
    }

    var _driverBase = function(){}
    _driverBase.prototype = {
        save : function(url){},
        load: function(url){},
        listDir:function(url){}
    }

    var drivers={

        xpConnect : function(){

            this.test = function(){return testCapabilities.xpConnect();}

            this.save = function(filePath,content,success,error){
                var file = Components.classes["@mozilla.org/file/local;1"].createInstance(Components.interfaces.nsILocalFile);
                file.initWithPath(filePath);
                if(!file.exists())
                file.create(0,0x01B4);// 0x01B4 = 0664
                var out = Components.classes["@mozilla.org/network/file-output-stream;1"].createInstance(Components.interfaces.nsIFileOutputStream);
                out.init(file,0x22,0x04,null);
                out.write(content,content.length);
                out.flush();
                out.close();
                return true;
            }

            this.load = function(filePath,success,error){
                var file = Components.classes["@mozilla.org/file/local;1"].createInstance(Components.interfaces.nsILocalFile);
                file.initWithPath(filePath);
                if(!file.exists()){error();}
                var inputStream = Components.classes["@mozilla.org/network/file-input-stream;1"].createInstance(Components.interfaces.nsIFileInputStream);
                inputStream.init(file,0x01,0x04,null);
                var sInputStream = Components.classes["@mozilla.org/scriptableinputstream;1"].createInstance(Components.interfaces.nsIScriptableInputStream);
                sInputStream.init(inputStream);
                var contents = sInputStream.read(sInputStream.available());
                sInputStream.close();
                inputStream.close();
                return contents;
            }
        },

        activeX: function(){
            var _self = this;
            var fso = new ActiveXObject("Scripting.FileSystemObject");

            this.test = function(){return testCapabilities.activeX();}

            this.createPathRecursive = function(path){
                //# Remove the filename, if present. Use trailing slash (i.e. "foo\bar\") if no filename.
                var pos = path.lastIndexOf("\\");
                if(pos==-1){pos = path.lastIndexOf("/");}
                if(pos!=-1){path = path.substring(0,pos+1);}
                //# Walk up the path until we find a folder that exists
                var scan = [path];
                var parent = fso.GetParentFolderName(path);
                while(parent && !fso.FolderExists(parent)) {
                    scan.push(parent);
                    parent = fso.GetParentFolderName(parent);
                }

                //# Walk back down the path, creating folders
                for(i=scan.length-1;i>=0;i--) {
                    if(!fso.FolderExists(scan[i])) {
                        fso.CreateFolder(scan[i]);
                    }
                }
                return true;
            }

            this.save = function(filePath,content,success,error){
                _self.createPathRecursive(filePath);
                var file = fso.OpenTextFile(filePath,2,-1,0);
                file.Write(content);
                file.Close();
            }

            this.load = function(filePath,success,error){
                var file = fso.OpenTextFile(filePath,1);
                var content = file.ReadAll();
                file.Close();
                return content;
            }

            this.copy = function(dest,source){
                _self.createPathRecursive(dest);
                fso.GetFile(source).Copy(dest);
            }
        },

        java: function(){

            var applet;

            if(document.applets[javaAppletName]){applet = document.applets[javaAppletName];}

            this.test = function(){return testCapabilities.java();}

            function javaUrlToFilename(url){
                var f = "//localhost";
                if(url.indexOf(f) == 0)
                return url.substring(f.length);
                var i = url.indexOf(":");
                return i > 0 ? url.substring(i-1) : url;
            }

            this.save = function(filePath,content,success,error){
                if(applet){
                    return applet.saveFile(javaUrlToFilename(filePath),"UTF-8",content);
                }else{
                    try{
                        var s = new java.io.PrintStream(new java.io.FileOutputStream(javaUrlToFilename(filePath)));
                        s.print(content);
                        s.close();
                        return true;
                    }catch(e){}
                }
                return false;
            }

            this.load = function(filePath,success,error){
                if(applet){
                    var ret = applet.loadFile(javaUrlToFilename(filePath),"UTF-8");
                    if(!ret){return null;}
                    return String(ret);
                }else{
                    try{
                        var content = [];
                        var r = new java.io.BufferedReader(new java.io.FileReader(javaUrlToFilename(filePath)));
                        var line;
                        while((line = r.readLine()) != null){content.push(String(line));}
                        r.close();
                    }catch(e){}
                }
                return false;
            }

        },

        local:function(){
            var _self = this;
            window.requestFileSystem = window.requestFileSystem || window.webkitRequestFileSystem;
            function errorHandler(e) {
                var msg = '';
                switch (e.code) {
                    case FileError.QUOTA_EXCEEDED_ERR:
                    msg = 'QUOTA_EXCEEDED_ERR';
                    break;
                    case FileError.NOT_FOUND_ERR:
                    msg = 'NOT_FOUND_ERR';
                    break;
                    case FileError.SECURITY_ERR:
                    msg = 'SECURITY_ERR';
                    break;
                    case FileError.INVALID_MODIFICATION_ERR:
                    msg = 'INVALID_MODIFICATION_ERR';
                    break;
                    case FileError.INVALID_STATE_ERR:
                    msg = 'INVALID_STATE_ERR';
                    break;
                    default:
                    msg = 'Unknown Error';
                    break;
                };
                console.log('Error: ' + msg);
            }

            this.test = function(){return testCapabilities.local();}

            this.save = function(filePath,content,success,error){
                alert('cannot use local storage');
            }

            this.load = function(filePath,success,error){
                alert('cannot use local storage');
            }

            window.webkitStorageInfo.requestQuota(PERSISTENT, 1024*1024,
                function(grantedBytes) {
                    window.requestFileSystem(PERSISTENT, grantedBytes,
                        function(fs){
                            _self.load = function(filePath,success,error){
                                fs.root.getFile(filePath, {create: true, exclusive: true},
                                    function(fileEntry) {
                                        fileEntry.file(
                                            function(file) {
                                                var reader = new FileReader();
                                                reader.onloadend = success;
                                                reader.readAsText(file);
                                            },
                                            error
                                        );
                                    },
                                    errorHandler
                                );
                            };
                            _self.save = function(filePath,content,success,error){
                                fs.root.getFile('log.txt', {create: true},
                                    function(fileEntry) {
                                    fileEntry.createWriter(
                                        function(fileWriter) {
                                            fileWriter.onwriteend = success;
                                            fileWriter.onerror = error;
                                            var bb = new BlobBuilder() || new WebKitBlobBuilder();
                                            bb.append(content);
                                            fileWriter.write(bb.getBlob('text/plain'));
                                        },
                                        error
                                    );
                                    },
                                    error
                                );
                            }
                        },
                        errorHandler
                    );
                },
                errorHandler
            );

        }
    }

    drivers.activeX.prototype = _driverBase.prototype;
    drivers.activeX.constructor = drivers.activeX;
    drivers.java.prototype = _driverBase.prototype;
    drivers.java.constructor = drivers.java;
    drivers.local.prototype = _driverBase.prototype;
    drivers.local.constuctor = drivers.local;
    drivers.xpConnect.prototype = _driverBase.prototype;
    drivers.xpConnect.constructor = drivers.xpConnect;
    _fileObject.prototype = _fileBase.prototype;
    _fileObject.constructor = _fileObject;
    _dirObject.prototype = _dirBase.prototype;
    _dirObject.constructor = _dirObject;

    function _jsio(){
        this.config = {
                userAgent : navigator.userAgent.toLowerCase(),
                browser:'',
                driverInterface:{
                    save: function(){},
                    load: function(){}
                },
                driver:''
            };
        var    _self = this;

        this.config.browser = this.detectBrowser(this.config.userAgent);
    }

    _jsio.prototype = {
        detectBrowser: function(userAgent){
            return {
                isIE: userAgent.indexOf("msie") != -1 && userAgent.indexOf("opera") == -1,
                isGecko: navigator.product == "Gecko" && userAgent.indexOf("WebKit") == -1,
                ieVersion: /MSIE (\d.\d)/i.exec(userAgent), // config.browser.ieVersion[1], if it exists, will be the IE version string, eg "6.0"
                isSafari: userAgent.indexOf("applewebkit") != -1,
                isBadSafari: !((new RegExp("[\u0150\u0170]","g")).test("\u0150")),
                firefoxDate: /gecko\/(\d{8})/i.exec(userAgent), // config.browser.firefoxDate[1], if it exists, will be Firefox release date as "YYYYMMDD"
                isOpera: userAgent.indexOf("opera") != -1,
                isChrome: userAgent.indexOf('chrome') > -1,
                isLinux: userAgent.indexOf("linux") != -1,
                isUnix: userAgent.indexOf("x11") != -1,
                isMac: userAgent.indexOf("mac") != -1,
                isWindows: userAgent.indexOf("win") != -1
            }
        },

        detectDriver: function(){
            var _driverName = '';
            if(testCapabilities.activeX()){
                return drivers.activeX();
            }else if(testCapabilities.xpConnect()){
                return drivers.xpConnect();
            }else if(testCapabilities.java()){
                return drivers.java();
            }else if(testCapabilities.local()){
                return drivers.local();
            }
            return null;
        },

        checkInterface: function(theObject, theInterface) {
            for (var member in theInterface) {
                if ( (typeof theObject[member] != typeof theInterface[member]) ) {
                    alert("object failed to implement interface member " + member);
                    return false;
                }
            }
            return true;
        },

        driver: function(driver){
            if(arguments.length){
                if(this.checkInterface(driver, this.config.driverInterface)){
                    this.config.driver = driver;
                }else{
                    console.log('wrong driver');
                }
                return this;
            }
            if(!this.config.driver){
                var _driver = this.detectDriver();
                if(_driver && this.checkInterface(_driver, this.config.driverInterface)){
                    this.config.driver = _driver;
                }else{
                    console.log('could not determin a suitable driver');
                    return false;
                }
            }
            return this.config.driver;

        },

        save: function(url,content,success){
            this.driver().save(url,content,function(){},this.error);
        },

        load:function(url,success){
            this.driver().load(url,success,this.error);
        },

        error:function(e){
            console.log(e);
        }
    }

    window.jsio = function(){return new _jsio();}


})();
\$\endgroup\$
  • \$\begingroup\$ Have you run your code through a linter such as jshint.com? \$\endgroup\$ – kojiro Dec 8 '11 at 23:53
  • 2
    \$\begingroup\$ This is pretty cool. A few suggestions: 1 - organize all those drivers into different files, and make a build file. There will probably be lots of uses for this where you'd just need one driver. 2 - try to avoid the browser detection. Capability detection plus falling back to other drivers on failure should be enough. \$\endgroup\$ – Dagg Dec 29 '11 at 4:35
5
\$\begingroup\$

First, I'll share some things I learned while tweaking twFile, a local file system plug-in for jQuery based on the filesystem.js portion of TiddlyWiki:

  • The number and frequency of security alerts vary with the browser and driver type. I found the signed TiddlySaver.jar driver to result in the best user experience (at most one security confirmation per browser session; often none) IE ActiveX was the worst, requiring three different confirmations, including one at each page load (file operation?).

  • Touching the DOM node holding the Java applet when the applet is still loading or failed to load (missing jar file) causes most browsers to freeze, at least in Windows with JRE 1.6. Simple sample. I worked around this by checking a flag in JavaScript land that was set from within the Java applet init() method, instead.

  • The Java applet can take some time to load, so it may not be available right away. My solution was to return a deferred object from the initialization method that is not resolved until a driver is found. I keep trying again after a brief 100ms timeout.

  • JavaLiveConnect seems to be nearly useless due to Java security policies. On browsers where it is supported, the best it can do is read the containing HTML file (no write).

  • To give an unsigned Java applet broader permissions, put the following file in your user home directory:

.java.policy

grant codeBase "file:/-" {
  permission java.io.FilePermission "/-", "read,write";
};

Quick review:

  • Do your load/save methods return a value or call a callback? It seems quite mixed, right now.
  • This one returns false instead of the java.io.FileReader's results:

code:

   this.load = function(filePath,success,error){
        if(applet){
            var ret = applet.loadFile(javaUrlToFilename(filePath),"UTF-8");
            if(!ret){return null;}
            return String(ret);
        }else{
            try{
                var content = [];
                var r = new java.io.BufferedReader(new java.io.FileReader(javaUrlToFilename(filePath)));
                var line;
                while((line = r.readLine()) != null){content.push(String(line));}
                r.close();
            }catch(e){}
        }
        return false;
    }
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.