Saturday, October 20, 2012

Modelling a file system in Javascript using the Composite pattern

A rather peculiar post this one. I've been building a terminal application using the interesting JQuery Terminal.  As part of this application, I needed to model a file system that could be created dynamically, with the usual suspects involved - directories, files, paths, permissions.

For my purposes, I also required the implementation to be 'pluggable' - that is, where the directory information and file content was sourced would be supplied by external objects.

I considered a few options, but opted finally for the simplest - using the composite pattern. So I have this trivial set of abstractions:

FileSystemResource
FileResource < FileSystemResource
DirectoryResource < FileSystemResource

where DirectoryResource (1) --- (*) FileSystemResource.

To provide a central point of access, there is a file system manager type, which in the example code only allows one to find a resource (directory/file) or change the current directory.

So here is the code in its entirety, and at the end of the post, some simple test statements. The first few lines are some helper bits and pieces, before starting with the definition of a simple permission.

 jfs = {};  
   
 // Inheritance helper  
 jfs.extend = function (childClass, parentClass) {  
      childClass.prototype = new parentClass;  
      childClass.prototype.constructor = childClass;  
      childClass.prototype.parent = parentClass.prototype;  
 };  
   
 jfs.fsConfig = {  
      rootDirectory: '/',  
      pathDelimiter: '/',  
      parentDirectory: '..',  
      currentDirectory: '.'  
 };   
   
 // Util  
 Array.prototype.first = function (match, def) {  
      for (var i = 0; i < this.length; i++) {  
          if (match(this[i])) {  
              return this[i];  
          }  
      }  
      return def;  
 };  
   
 String.prototype.splitButRemoveEmptyEntries = function (delim) {  
      return this.split(delim).filter(function (e) { return e !== ' ' && e !== '' });  
 };  
    
 // Simple permissions group  
 jfs.fileSystemPermission = function (readable, writeable) {  
      this._readable = readable;  
      this._writeable = writeable;  
 };  
 jfs.fileSystemPermission.prototype.writeable = function () {  
      return this._writeable;  
 };  
 jfs.fileSystemPermission.prototype.readable = function () {  
      return this._readable;  
 };  
 jfs.fileSystemPermission.prototype.toString = function () {  
      return (this.readable() ? 'r' : '-').concat((this.writeable() ? 'w' : '-'), '-');  
 };  
 jfs.standardPermission = new jfs.fileSystemPermission(true, false);  
 // Base resource  
 jfs.fileSystemResource = function () {  
      this._parent = undefined;  
      this._tags = {};  
 };  
 jfs.fileSystemResource.prototype.init = function (name, permissions) {  
      this._name = name;  
      this._permissions = permissions;  
      return this;  
 };  
 // Return the contents of the receiver i.e. for cat purposes  
 jfs.fileSystemResource.prototype.contents = function (consumer) {  
 };  
 // Return the details of the receiver i.e. for listing purposes  
 jfs.fileSystemResource.prototype.details = function (consumer) {  
      return this.toString();  
 };  
 jfs.fileSystemResource.prototype.name = function () {  
      return this._name;  
 };  
 jfs.fileSystemResource.prototype.getParent = function () {  
      return this._parent;  
 };  
 jfs.fileSystemResource.prototype.getTags = function () {  
      return this._tags;  
 };  
 jfs.fileSystemResource.prototype.setParent = function (parent) {  
      this._parent = parent;  
 };  
 jfs.fileSystemResource.prototype.permissions = function () {  
      return this._permissions;  
 };  
 jfs.fileSystemResource.prototype.type = function () {  
      return '?';  
 };  
 jfs.fileSystemResource.prototype.find = function (comps, index) {  
 };  
 jfs.fileSystemResource.prototype.absolutePath = function () {  
      return !this._parent ? '' :   
         this._parent.absolutePath().concat(jfs.fsConfig.pathDelimiter, this.name());  
 };  
 jfs.fileSystemResource.prototype.toString = function () {  
      return this.type().concat(this._permissions.toString(), ' ', this._name);  
 };  
 // Directory  
 jfs.directoryResource = function () {  
      this.children = [];  
 };  
 jfs.extend(jfs.directoryResource, jfs.fileSystemResource);  
 jfs.directoryResource.prototype.contents = function (consumer) {  
      return '';  
 };  
 jfs.directoryResource.prototype.details = function (consumer) {  
      consumer('total 0');  
      this.applyToChildren(function (kids) { kids.forEach(function(e) { consumer(e.toString()); }) });  
 };  
 jfs.directoryResource.prototype.type = function () {  
      return 'd';  
 };  
 jfs.directoryResource.prototype.addChild = function (resource) {  
      this.children.push(resource);  
      resource.setParent(this);  
      return this;  
 };  
 jfs.directoryResource.prototype.applyToChildren = function (fn) {  
      return this._proxy && this.children.length == 0 ? this._proxy.obtainState(this, fn) : fn(this.children);  
 };  
 jfs.directoryResource.prototype.setProxy = function (proxy) {  
      this._proxy = proxy;  
 };  
 jfs.directoryResource.prototype.find = function (comps, index) {  
      var comp = comps[index];  
      var node = comp === '' || comp === jfs.fsConfig.currentDirectory ? this :  
                (comp === jfs.fsConfig.parentDirectory ? this.getParent() :   
                this.applyToChildren(function(kids) { return kids.first(function(e) { return e.name() === comp; }); }));  
      return !node || index === comps.length - 1 ? node : node.find(comps, index + 1);  
 };  
 // File  
 jfs.fileResource = function () {  
 };  
   
 jfs.extend(jfs.fileResource, jfs.fileSystemResource);  
 // consumer should understand:   
 // accept(obj) - accept content  
 // failed   - producer failed, totally or partially  
 jfs.fileResource.prototype.contents = function (consumer) {  
      this._producer(this, consumer || this._autoConsumer);  
 };  
   
 jfs.fileResource.prototype.type = function () {  
      return '-';  
 };  
   
 jfs.fileResource.prototype.plugin = function (producer, autoConsumer) {  
      this._producer = producer;  
      this._autoConsumer = autoConsumer;  
 };  
   
 // FSM  
 jfs.fileSystemManager = function () {  
      this._root = new jfs.directoryResource();  
      this._root.init('', jfs.standardPermission);  
      this._currentDirectory = this._root;  
 };  
   
 jfs.fileSystemManager.prototype.find = function (path) {  
      var components = path.splitButRemoveEmptyEntries(jfs.fsConfig.pathDelimiter);  
      if (components.length === 0) components = [ '.' ];  
      return (path.substr(0, 1) === jfs.fsConfig.rootDirectory ? this._root : this._currentDirectory).find(components, 0);  
 };  
   
 jfs.fileSystemManager.prototype.currentDirectory = function () {  
      return this._currentDirectory;  
 };  
   
 jfs.fileSystemManager.prototype.root = function () {  
      return this._root;  
 };  
   
 jfs.fileSystemManager.prototype.changeDirectory = function (path) {  
      var resource = this.find(path);  
      if (resource) this._currentDirectory = resource;  
      return resource;  
 };  

And the test code; it creates a directory under the root called 389, and adds a file (called TestFile) to that directory, plugging in an example 'producer' function (that knows how to get the content of this type of file object) and an auto consumer - that is, a default consumer attached to the object. It is possible to pass any object in when calling the contents function and to not use default consumers at all.

Finally, and for illustration only, we use the file system manager find function to get the actual resource denoted by the full path name, and ask it for its contents. As we have an auto consumer associated with the object, it executes. In this case, we would dump two lines to the console log; 'some' and 'content'.

1:  var m = new jfs.fileSystemManager();   
2:  var d = new jfs.directoryResource();   
3:  d.init('389', jfs.standardPermission);   
4:  m.currentDirectory().addChild(d) ;   
5:  var f = new jfs.fileResource();   
6:  f.init('TestFile', jfs.standardPermission);   
7:  d.addChild(f);   
8:  f.plugin(function(fileResource, consumer) {   
9:         ['some', 'content'].forEach(function(e) { consumer(e) })  
10:      },   
11:      function(e) { console.log(e) });  
12:  var r = m.find('/389/TestFile');  
13:  r.contents();  
14:    

No comments: