sanity check
[henge/kiak.git] / src / strappFileSystem.js
index 6f0b9d4..5b2df7b 100644 (file)
 /**
- * @file      File System Interface
- * @desc      Provides basic file commands for interacting with Strapp
- *            file system as well as storage (and backups) of file system
- * @author    Jordan Lavatai and Ken Grimes
- * @version   0.0.1
- * @license   AGPL-3.0
- * @copyright Strapp.io
- */
-
-import localforage from "localforage"
-import StrappPeerConnection from "strappPeerConnection"
+* @file      Strapp File System
+* @author    Jordan Lavatai, Ken Grimes
+* @version   0.0.1
+* @license   AGPL-3.0
+* @copyright August 2017 - Ken Grimes, Jordan Lavatai
+* @summmary  File system implementation for a strapp node
+*/
+import strapp from './strapp.js'
+import localforage from 'localforage'
+localforage.configure({
+  name: 'strapp',
+  version: 0.1
+})
+/** extend localforage with an assign operation */
+localforage.assignItem = (path, data) => new Promise((resolve, reject) => {
+  localforage.getItem(path, data).then((fdata) => {
+    localforage.setItem(path, Object.assign(fdata, data))
+      .then(resolve).catch(reject)
+  }).catch((err) => {
+    localforage.setItem(path, data)
+      .then(resolve).catch(reject)
+  })
+})
+localforage.getOrSetItem = (path, defaults) => new Promise((resolve, reject) => localforage.getItem(path).then(resolve).catch((err) => localforage.setItem(path, defaults).then(resolve).catch(reject)))
+
+
+const strappRuntime = {
+  run: new StrappDirectory({
+    perm: 0xFFF4,
+    files: {
+      keyboard: new StrappDevice(),
+      mouse: new StrappDevice(),
+      touch: new StrappDevice(),
+      client: new StrappDirectory({
+        perm: 0xFF0,
+        files: {
+          create: new StrappFile({
+            GET(){},
+            POST(){} //TODO: create client action
+          })
+        }
+      }),
+      relay: new StrappDirectory({
+        perm: 0x000,
+        files: {
+          create: new StrappFile({
+            GET(){},
+            POST(){} //TODO: create relay
+          })
+        }
+      }),
+      spc: new StrappDirectory({
+        perm: 0xF00,
+        files: {
+          create: new StrappFile({
+            GET(){},
+            POST(){} //TODO: spc creation (probably only internal)
+          })
+        }
+      })
+    }
+  })
+}
 
-/* File constructor */
-class File extends Object {
-  constructor(...props) {
-    super()
-    return Object.assign(this, new.target.defaults, ...props)
+const StrappFileSystem = (() => {
+  /* Internal State */
+  let rootUser
+  const rootDir = new StrappDirectory(StrappFile.literal({
+    type: 'strapp/directory',
+    perm: 0xFF4,
+    path: '.'
+  }))
+
+  /* Internal Functions */
+  const _genKeyPair = () => {
+    //TODO
+    return {
+      pubKey: '',
+      privKey: ''
+    }
   }
-  get() {
-    return this.data
+  const _request = (location, method, data) =>
+        rootDir.request(location.split('/'), method, rootUser.pubKey, data)
+
+  /* API Definition */
+  const StrappFileSystem = {
+    request: (location, method, data) =>
+      new Promise((resolve, reject) => this.loadWaiters.push([
+        location, method, data, resolve, reject
+      ])),
+    get: (location) => StrappFileSystem.request(location, 'GET'),
+    set: (location, data) => StrappFileSystem.request(location, 'PUT', data),
+    resolveRootUser: (path) =>
+      localforage.getItem(path)
+      .then((data) => Promise.resolve,
+            (err) => localforage.setItem(path, _genKeyPair()))
+      .then((data) => Promise.resolve(rootUser = data))
   }
-  post(postedData) {
-    this.data += postedData
-    this.lastModified = new Date()    
+  StrappFileSystem.loadWaiters = []
+  StrappFileSystem.bootTime = new Date()
+
+  /* Init sequence */
+  StrappFileSystem.resolveRootUser('acct/local')
+    .then((data) => Promise.all(
+      [
+        ['.', {
+          acct: StrappDirectory.literal()
+        }],
+        ['acct', {
+          local: StrappFile.literal()
+        }]
+      ].map((entry) => localforage.getOrSetItem(entry[0],entry[1]))
+    ))
+    .then((loadedFiles) => {
+      rootDir.loadFiles(loadedFiles[0])
+      StrappFileSystem.request = _request
+      StrappFileSystem.loadWaiters
+        .map((w) => _request(w[0], w[1], w[2], w[3]).then(w[4], w[5]))
+    })
+  
+  return StrappFileSystem
+})()
+const StrappFile = (() => {
+  class StrappFile extends Object {
+    constructor(...props) {
+      super()
+      return Object.assign(this, new.target.defaults, ...props)
+    }
+    static literal(...props) {
+      return Object.assign(Object.create(null, StrappFile.defaults), ...props)
+    }
+    static parse(fileLiteral) {
+      switch(fileLiteral.type) {
+      case 'application/JSON':
+        return new StrappJSON(fileLiteral)
+      case 'strapp/directory':
+        return new StrappDirectory(fileLiteral)
+      case 'strapp/file':
+      default:
+        return new StrappFile(fileLiteral)
+      }
+    }
+    resolveRequest(method, pubKey, data, locStack) {
+      if (!locStack)
+        return Promise.reject(400)
+      if (locStack.length > 0)
+        return Promise.reject(404)
+      let reqPerms = 0
+      switch(method) {
+      case 'OPTIONS':
+        return new Promise((resolve, reject) => {
+          this.resolveClientPerms.then((perms) => {
+            let allow = ['OPTIONS']
+            if (perms & 0x9)
+              allow.push('CONNECT')
+            if (perms & 0x2)
+              allow.push('DELETE').push('PUT').push('POST')
+            if (perms & 0x4)
+              allow.push('GET').push('HEAD')
+            resolve(allow.join(', '))
+          }).catch(reject)
+        })
+      default:
+        return Promise.reject(405)
+      case 'PUT':
+      case 'POST':
+      case 'DELETE':
+        reqPerms = 0x2
+        break
+      case 'GET':
+      case 'HEAD':
+        reqPerms = 0x4
+        break
+      case 'CONNECT':
+        reqPerms = 0x9
+        break
+      }
+      return new Promise((resolve, reject) => {
+        this.resolveClientPerms.then((perms) => {
+          if ((reqPerms & perms) === reqPerms)
+            this[method](pubKey, data).then(resolve).catch(reject)
+          else
+            reject(401)
+        }).catch(reject)
+      })
+    }
+    resolveClientPerms(pubKey) {
+      return new Promise((resolve, reject) => {
+        if (!pubKey || pubKey === '')
+          resolve(this.perms >>> 12 & 0xF)
+        else if (pubKey === this.owner)
+          resolve((this.perms >>> 8) & 0xF)
+        else
+          localforage.getItem(`acct/${pubKey}`).then((account) => {
+            let grpLen = account.groups ? account.groups.length : 0
+            let found = false
+            for (let i = 0; i < grpLen; i++) {
+              if (account.groups[i] === this.group) {
+                resolve((this.perms >>> 4) & 0xF)
+                found = true
+                break
+              }
+            }
+            if (!found)
+              resolve(this.perms & 0xF)
+          }).catch(reject)
+      })
+    }
+    HEAD(pubKey) {
+      return Promise.resolve()
+    }
+    GET(pubKey) {
+      return localforage.getItem(this.path)
+    }
+    PUT(pubKey, data) {
+      return localforage.setItem(this.path, data)
+    }
+    POST(pubKey, data) {
+      return new Promise((resolve, reject) => {
+        localforage.getItem(this.path)
+          .then((fData) =>
+                this.setItem(this.path, fData + data)
+                .then(resolve)
+                .catch(reject)
+               )
+          .catch(reject)
+      })
+    }
+    DELETE(pubKey) {
+      return localforage.removeItem(this.path)
+    }
+    OPTIONS(pubKey) {
+      return new Promise((resolve, reject) => {
+        this.resolveClientPerms(pubKey).then((perms) => {
+        }).catch(reject)
+      })
+    }
+    CONNECT(pubKey) { 
+      return this.GET(pubKey)
+    }
+    TRACE(opt) {
+    }
+    PATCH(opt) {
+    }
   }
-  put(putData) {
-    this.data = putData
-    this.lastModified = new Date()
+  StrappFile.defaults = {
+    type: 'strapp/file',
+    perm: 0xF00,
+    owner: 'local',
+    group: '',
+    changed: StrappFileSystem.bootTime,
+    created: StrappFileSystem.bootTime,
+    accessed: StrappFileSystem.bootTime,
+    files: undefined
   }
-  delete() {
-    this.data = ''
-    this.lastModified = new Date()
+  return StrappFile
+})()
+
+const StrappPeerConnection = (() => {
+  class StrappPeerConnection extends StrappFile {
+    GET(opts) {
+      //get metadata (held in filesystem), with owner, usage info, etc
+      //if unauthed, send message down socket
+    }
+    PUT(opts) {
+      //create w/ sdp, register callback (or pipe), set owner
+    }
+    POST(opts) {
+      //send msg
+    }
+    routeMessage(msg) {
+      //send routing message down socket
+      //POST(opts.routemessage)
+    }
   }
-  connect() {
-    
+  return StrappPeerConnection
+})()
+
+const StrappDirectory = (() => {
+  const _traverse_loaded = function(method, pubKey, data, locStack) {
+    if (locStack[0] in this.files)
+      return this.files[locStack[0]].resolveRequest(method, pubKey, data, locStack.slice(1))
+    return Promise.reject(404)
   }
-  options(publicKey) {
-    return this.availPermissions(publicKey)
+  const  _traverse_loading = function(method, pubKey, data, locStack) {
+    if (this.loadWaiters)
+      return new Promise((resolve, reject) => this.loadWaiters.push(
+        [method, pubKey, data, locStack, resolve, reject]
+      ))
+    return _traverse_loaded(method, pubKey, data, locStack)
   }
-}
-/* TODO: Continue to flesh this out */
-File.defaults = {
-  name: '',
-  data: '',
-  mode: {}, 
-  size: 0
-  //lastModified: new Date()?
-  //lastAccessed: new Date()?
-
-}
-
-
-/* Filesystem maintains the current session memory for the strapp instance. Files can be
-   created and killed from the filesystem without leveraging localForage. Files that are
-   in the filesystem can be stored to localForage while files that are in localForage can be loaded
-   to the filesystem. When a Filesystem is first intialized, it attempts to get its strappID it populates itself from localForage and
-   overwrites any files in its current memory. Files that have restore() as a property (which will be some method needed to 
-   make the file functions e.g. strappPeerConnections will restore() themselves lazily, i.e. when they are needed.*/
-
-   /* TODO: Should it be possible to create/preserve/destroy a file to both localForage and fileSystem? (same time) */
-   /* TODO: Should initFileSystem not overwrite files? */ 
-
-/* These are the default files on all file systems */
-let defaultFiles = [ "..", ".", "accounts", "ice", "log", "run" ]
-
-
-/* TODO: Protect data via closures? */
-const fs = {
-    /* TODO: What if files are added to file system before init is is called? */
-  initFileSystem(){
-    this.db = localforage.createInstance({ name: "database" })
-    /* Iterate through all files on localforage, adding them to FileSystem
-       and calling their restore methods */
-    this.db.iterate( (value, key, n) => {
-      /* just btw, return !undefined to exit early */
-      this.loadFile(key, true)
-      
-    }).catch( (err) => {
-      console.log(`error: ${err} when iterating through localForage during initFileSystem`)
-    })
-    /* Add the hardcoded default files if they dont already exist */
-    /* Restore these files --> need the private/public key*/
-    initialFiles.map( (val, idx, array) => {
-      if (this.fileExists(val)) {
-       let file = this.getFile(val)
-       let restoreProp = file['restore']
-       if (restoreProp === undefined && typeof restoreFx === 'function') {
-         //restore file
-       }
-       /* Else don't do anything, file exists in FS and doesnt need to be restored */          
-      }
-      else {
-       /* TODO: Remove checking for every file --> although its only for the default files which
-                 will probably be a low number. Still, unnecessary. Could make initialFiles a 
-                 Map object with fileType in it and switch(val.fileType)  */
-       if (val === '..') {
-         let file = new StrappPeerConnection()
-         /* Connect with host */
-       }
-       else {
-         /* Each default file is going to have specific permissions,  */
-         let filedata = new File()
-         filedata.name = val
-         filedata.mode = 
-           this.createFile(val,) 
-       }
-      }
-      
-    })
-    
-  },
-
-  fileExists(filename) {
-    return this.files[filename] === undefined ? false : true
-  },
-
-  /* Create a file in the file system, if specified overwrites any file that is already there
-     else does nothing */
-  createFile(filename, filedata, overwrite = false){
-    filedata.name = filename
-    if (this.files[filename] === undefined) {
-      this.files[filename] = filedata
-    }
-    else {
-      if (overwrite) {
-        console.log(`Overwriting ${filename}`)
-       this.files[filename] = filedata
-      }
-      else {
-       console.log(`Didn't overwrite file so nothing happened`)
-      }
-    }
-  }, 
-
-  /* Get a file from browser session memory */
-  /* TODO: Option to get from localForage? */
-  getFile(filename) {
-    return this.files[filename]
-  },
-
-  /* Save a file to file system*/
-  saveFile(filename, filedata) {    
-    /* TODO: Determine if file to be saved is of saveable nature e.g. SPC's cant really be saved */
-    this.db.setItem(filename, filedata)
-  },
-  
-  /* Delete file from localForage */
-  removeFile(filename) {
-    this.db.removeItem(filename)
-  },
-
-  /* Delete a file from file system */
-  killFile(filename) {
-    delete this.files[filename]
-  
-  },
-
-  /* Store file in file system to localForage */
-  storeFile(filename) {
-    this.db.setItem(filename, this.files[filename].filedata)
-  },
-
-  /* Load file from localForage to file system */
-  loadFile(filename, overwrite = false) {
-  let filedata = this.db.getItem(filename)
-  filedata = filedata.restore === undefined ? filedata : filedata.restore()
-    this.createFile(filename, filedata, overwrite)
-  },
-  
-  saveFileSystem() {
-    /* TODO: save all files in filesystem to localforage */
-  },
-  db: {},
-  files: {}
-  
-}
-
-/* File System API */
-
-
-/* Load file system from localStorage/indexedDB if strapp.js is running on a host */
-function loadFileSystem(){
   
-}
-
-/* Store file system before shutting down if strapp.js is running on a host- */
-function storeFileSystem(){}
-
-
-
-/* addFile - adds a created file to a file */
-//@arg fileType - what to set the type property
-//@arg fileData - what to set the data property
-//@arg filePos - where to create the file
-
-/* Determine if a publicKey has permissions to execute methods
-
-/* rm - Delete file */
-
-/* ls - display file contents */
-  //open file
-//return all of file files
-
-/* cat - display file data */
-//return file data
-
-/* open - Open file */
-  //traverse to file path
-/* perm - display file permissions (if you have permissions) */
-/* find - find a file */
-//@arg fileToFind
-/* stat - info about a file */
-//@arg path - path to the file
-/* exists - determine if a file contains a file */
-//@arg {String} searchedFile - file to be searched
-//@arg {String} fileName
-//@arg {Number} Depth to look
-//@return {Boolean} true if exists, false if doesn't
-
-
+  class StrappDirectory extends StrappFile {
+    static literal(...props) {
+      return Object.assign(Object.create(null, StrappDirectory.defaults), ...props)
+    }
+    loadFiles(fileData) {
+      Object.keys(fileData)
+        .map((key) => (this.files[key] = StrappFile.parse(fileData[key])))
+    }
+    resolveOwnFiles() {
+      return new Promise((resolve, reject) => {
+        localforage.getItem(this.path).then((fileData) => {
+          this.loadFiles(fileData)
+          resolve()
+        }).catch(reject)
+      })
+    }
+    traverse(method, pubKey, data, locStack) {
+      this.traverse = _traverse_loading
+      this.loadWaiters = []
+      this.resolveOwnFiles().then(() => {
+        this.traverse = _traverse_loaded
+        this.loadWaiters
+          .map((w) => _traverse_loaded(w[0], w[1], w[2], w[3]).then(w[4], w[5]))
+        delete this.loadWaiters
+      })
+      return _traverse_loading(method, pubKey, data, locStack)
+    }
+    request(method, pubKey, data, locStack) {
+      return new Promise((resolve, reject) => {
+        super.resolveRequest(method, pubKey, data, locStack)
+          .then(resolve)
+          .catch((err) => {
+            if (err === 404)
+              return this.traverse(method, pubKey, data, locStack)
+            return Promise.reject(err)
+          })
+      })
+    }
+    CONNECT(opts) {
+      //send routing message to the directory (handle the next part here)
+    }
+  }
+  StrappDirectory.defaults = {
+    type: 'strapp/directory'
+  }
+  return StrappDirectory
+})()
 
+const StrappDevice = (() => {
+  class StrappDevice extends StrappFile {
+  }
+  return StrappDevice
+})()
 
+const StrappJSON = (() => {
+  class StrappJSON extends StrappFile {
+    POST(pubKey, data) {
+      return localforage.assignItem(this.path, data)
+    }
+  }
+  return StrappJSON
+})()
 
+export default StrappFileSystem
+export { StrappFile, StrappPeerConnection, StrappDirectory }