sanity check
[henge/kiak.git] / src / strappFileSystem.js
index 00f1050..5b2df7b 100644 (file)
 * @copyright August 2017 - Ken Grimes, Jordan Lavatai
 * @summmary  File system implementation for a strapp node
 */
-import LocalForage from "localforage"
-
-const StrappFile = (() => {
-  const localforage = LocalForage.createInstance({
-    driver: [LocalForage.LOCALSTORAGE,
-             LocalForage.INDEXEDDB,
-             LocalForage.WEBSQL],
-    name: 'strapp',
-    version: 0.1,
-    storeName: 'strapp'
+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)
   })
-  const authorize = (pubKey, mode, stat) => {
-    let allowed
-    if (pubKey === stat.owner)
-      allowed = (stat.perms >>> 16) & 0xF
-    else {
-      let gAccess = false
-      let uGroups = StrappFile.get(`acct/${pubKey}/groups`).split(' ')
-      for (let i = 0; i < uGroups.length; i++) {
-        if (uGroups[i] === stat.group) {
-          gAccess = true
-          break
+})
+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
+          })
         }
-      }
-      if (gAccess)
-        allowed = (stat.perms >>> 8) & 0xF
-      else
-        allowed = stat.perms & 0xF
-    }
-    switch(mode){
-    case 'r+':
-    case 'rw':
-    case 'wr':
-      return (allowed & 0x6) === 0x6
-    case 'w':
-      return (allowed & 0x2) === 0x2
-    case 'r':
-      return (allowed & 0x4) === 0x4
-    case 'x':
-      return (allowed & 0x1) === 0x1
-    default:
-      console.log(`Unknown access mode: ${mode}`)
-      return false
+      }),
+      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)
+          })
+        }
+      })
     }
+  })
+}
+
+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: ''
+    }
+  }
+  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))
   }
+  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 PermissionDenied() {
-      return new Promise((resolve, reject) => reject('Permission denied'))
-    }
-    static get(path) {
-      return localforage.getItem(path)
+    static literal(...props) {
+      return Object.assign(Object.create(null, StrappFile.defaults), ...props)
     }
-    static set(path, data) {
-      return localforage.setItem(path, data)
+    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)
+      }
     }
-    static delete(path) {
-      return localforage.removeItem(path)
+    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)
+      })
     }
-    static routeMessage(lmkid) {
-      //split lmkid by spaces
-      //regex sanitize.  if '/', MSG.  else if ' ', resolve method
+    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(opt) {
-      if (authorize(opt.pubKey, 'r', this.stat))
-        return new Promise((resolve, reject) => resolve(''))
-      else
-        return StrappFile.PermissionDenied()
+    HEAD(pubKey) {
+      return Promise.resolve()
     }
-    GET(opt) {
-      if (authorize(opt.pubKey, 'r', this.stat))
-        return StrappFile.get(this.path)
-      else return StrappFile.PermissionDenied()
+    GET(pubKey) {
+      return localforage.getItem(this.path)
     }
-    PUT(opt) {
-      if (authorize(opt.pubKey, 'w', this.stat))
-        return StrappFile.set(this.path, opt.data)
-      else return StrappFile.PermissionDenied()
+    PUT(pubKey, data) {
+      return localforage.setItem(this.path, data)
     }
-    POST(opt) {
-      return this.PUT(Object.assign(opt, { data: this.GET(opt) + opt.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(opt) {
-      if (authorize(opt.pubKey, 'w', this.stat))
-        return StrappFile.delete(this.path)
-      else return StrappFile.PermissionDenied()
+    DELETE(pubKey) {
+      return localforage.removeItem(this.path)
     }
-    OPTIONS(opt) {
-      return this.stat
+    OPTIONS(pubKey) {
+      return new Promise((resolve, reject) => {
+        this.resolveClientPerms(pubKey).then((perms) => {
+        }).catch(reject)
+      })
     }
-    CONNECT(opt) { //make channel
-      return this.GET(opt)
+    CONNECT(pubKey) { 
+      return this.GET(pubKey)
     }
     TRACE(opt) {
     }
@@ -108,15 +246,14 @@ const StrappFile = (() => {
     }
   }
   StrappFile.defaults = {
-    stat: {
-      type: 'mime/type',
-      perm: 0,
-      owner: 'thisOwnerPubKey',
-      group: 'groupname',
-      changed: 'time',
-      created: 'time',
-      accessed: 'time - not saved'
-    }
+    type: 'strapp/file',
+    perm: 0xF00,
+    owner: 'local',
+    group: '',
+    changed: StrappFileSystem.bootTime,
+    created: StrappFileSystem.bootTime,
+    accessed: StrappFileSystem.bootTime,
+    files: undefined
   }
   return StrappFile
 })()
@@ -133,7 +270,7 @@ const StrappPeerConnection = (() => {
     POST(opts) {
       //send msg
     }
-    MSG(opts) {
+    routeMessage(msg) {
       //send routing message down socket
       //POST(opts.routemessage)
     }
@@ -142,15 +279,81 @@ const 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)
+  }
+  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)
+  }
+  
   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 StrappFile
-export { StrappPeerConnection, StrappDirectory }
+export default StrappFileSystem
+export { StrappFile, StrappPeerConnection, StrappDirectory }