file.js

const Path = require('path')
const crypto = require('crypto')
const Hoek = require('@hapi/hoek')
const ObjectId = require('bson-objectid')
const debug = require('debug')('gpgfs.File')

const Utils = require('./utils')

  /** Class representing gpgfs File */
class File {
  
  /** @hideconstructor */
  constructor({bucket, id, filePath}){
    this.bucket = bucket
    this.id = new ObjectId(id)
    this.filePath = Path.normalize(Path.join('/', filePath))

    this.content = ''
    this.metadata = null
    this.lastchange = null
  }

  /**
   * Open and read metadata 
   * @method */
  async open(){
    /*await this.getMetadata()
    await this.getLastchange()*/
    //this.filePath = this.metadata.path
    debug('loaded ', this.filePath)
  }

  async delete(){
    debug('deleting file', this.filePath)
    await this.bucket.releaseFile(this)
    await this.bucket.unindexFile(this)
    await this.bucket.root.rmFile(this.path)
    await this.bucket.root.rmFile(this.metadataPath)
    await this.bucket.root.rmFile(this.lastchangePath)

    await this.release()
    delete this
  }

  async release(){
    debug('releasing', this.bucket.id.toString(), '/', this.id.toString())
    delete this.content
    delete this.metadata
    delete this.lastchange

    this.content = ''
    this.metadata = null
    this.lastchange = null
  }

  /**
   * Check if all metadata and content exists on disk
   * @method
   * @returns {boolean} */
  async exists(){
    const contentExists = await this.bucket.root.fileExists( this.path )
    
    /*const metadataExists = this.bucket.root.fileExists( this.lastchangePath )
    const lastchangeExists = this.bucket.root.fileExists( this.path )*/

    return  contentExists
  }
  
  async existsFully(){
    const existance = await Promise.all([
      this.bucket.root.fileExists( this.path ),
      this.bucket.root.fileExists( this.metadataPath ),
      this.bucket.root.fileExists( this.lastchangePath )
    ])

    return existance[0] && existance[1] && existance[2]
  }


  get path(){
    return Path.join(
      this.bucket.path,
      'objects/object-' + this.id.toHexString()
    )
  }

  get metadataPath(){
    return Path.join(
      this.bucket.path,
      'object-meta/object-' + this.id.toHexString() +'-meta'
    )
  }

  get lastchangePath(){
    return Path.join(
      this.bucket.path,
      'object-lastchange/object-' + this.id.toHexString() +'-lastchange'
    )
  }

  /**
   * Create file if it does not exist 
   * @method
   * @param {string} [value=''] Content to save in file
   */
  async create(value){
    debug('create -', this.id)
    if(await this.exists()){ throw new Error('file exists') }

    await this.setMetadata()
    await this.save(value)
  }

  async getReciepents(){
    await this.bucket.root.cacheWhoami()
    const bucketToList = await this.bucket.getReciepents()
    let toList = [ ...bucketToList ]

    if(this.metadata){

      if(this.metadata.meta && this.metadata.meta.length > 0){
        toList = toList.concat(this.metadata.meta)
      }

      if(this.metadata.readers && this.metadata.readers.length > 0){
        toList = toList.concat(this.metadata.readers)
      }

      if(this.metadata.writers && this.metadata.writers.length > 0){
        toList = toList.concat(this.metadata.writers)
      }

    }

    return Utils.uniqueArray(toList)
  }

  /**
   * Read content from file 
   * @method
   * @returns {string}
   */
  async read(){
    //load & decrypt
    this.content = await this.bucket.root.readFile( this.path, true, null, this.bucket.readKeychain, {from: this.bucket.metadata.writers})
    return this.content
  }

  /**
   * Write content to file. Update metadata and re-index file in bucket index.
   * @method 
   * @param {string} [newContent=this.content]
   */
  async save(newContent=null){

    if(await this.existsFully()){
      await Promise.all([
        await this.getMetadata(),
        await this.getLastchange()
      ])
    }
    
    const contentReaders = Utils.uniqueArray(
      [ this.metadata.owner ].concat(
        this.metadata.readers,
        this.metadata.writers,
        Hoek.reach(this, 'bucket.metadata.readers', {default: []}),
        Hoek.reach(this, 'bucket.metadata.writers', {default: []})
      )
    )

    if(newContent){
      this.content = newContent
    }

    await this.bucket.root.writeFile( this.path,
      this.content,
      {
        encrypt: true,
        trust: 'direct',
        to: contentReaders
      }
    )

    await this.updateLastChange()

    await this.bucket.indexFile(this)
  }

  /**
   * File metadata 
   * @method 
   * @returns {gpgfs_model.object_meta} See [`gpgfs_model.object_meta`]{@link https://github.com/datapartyjs/gpgfs-model/blob/master/src/types/object_meta.js}
   */
  async getMetadata(){
    if(this.metadata){return}
    this.metadata = await this.bucket.root.readFile( this.metadataPath, true, 'object_meta', this.bucket.metaKeychain, {from: this.bucket.metadata.writers})
    return this.metadata
  }

  async setMetadata(){
    const toList = await this.getReciepents()
    const nowTime = (new Date()).toISOString()
    let newMetadata = Object.assign({lastchanged: nowTime}, {
      owner: this.bucket.metadata.owner,
      path: Path.normalize( this.filePath ),
      bucketId: {
        id: this.bucket.id.toHexString(),
        type: 'bucket_meta'
      },
      objectId: {
        id: this.id.toHexString(),
        type: 'object_meta'
      },
      created: Hoek.reach(this, 'metadata.created') || nowTime,
      cleartext: Hoek.reach(this, 'bucket.metadata.cleartext'),
      meta: Hoek.reach(this, 'bucket.metadata.meta'),
      readers: Hoek.reach(this, 'bucket.metadata.readers'),
      writers: Hoek.reach(this, 'bucket.metadata.writers')
    })

    await this.bucket.root.writeFile( this.metadataPath,
      newMetadata,
      {
        model: 'object_meta',
        encrypt: true,
        trust: 'direct',
        to: await this.getReciepents()
      }
    )

    if(!this.metadata){
      debug('creating metadata')
      this.metadata = newMetadata
    }
    else {
      debug('replacing metadata')
      this.metadata = newMetadata
    }
  }

  /**
   * Last change metadata 
   * @method 
   * @returns {gpgfs_model.object_lastchange} See [`gpgfs_model.object_lastchange`]{@link https://github.com/datapartyjs/gpgfs-model/blob/master/src/types/object_lastchange.js}
   */
  async getLastchange(){
    if(this.lastchange){return}
    this.lastchange = await this.bucket.root.readFile( this.lastchangePath, true, 'object_lastchange', this.bucket.metaKeychain, {from: this.bucket.metadata.writers})
    return this.lastchange  
  }
  
  async updateLastChange(){
    const nowTime = (new Date()).toISOString()

    const hash = {
      sha256: crypto.createHash('sha256').update(this.content).digest('hex')
    }

    await this.bucket.root.cacheWhoami()

    let newLastchange = Object.assign({lastchanged: nowTime}, {
      bucketId: {
        id: this.bucket.id.toHexString(),
        type: 'bucket_meta'
      },
      objectId: {
        id: this.id.toHexString(),
        type: 'object_meta'
      },
      size: this.content.length,
      actor: this.bucket.root.whoami,
      //hash
    })

    debug('updateLastChange -', newLastchange)

    await this.bucket.root.writeFile( this.lastchangePath,
      newLastchange,
      {
        model: 'object_lastchange',
        encrypt: true,
        trust: 'direct',
        to: await this.getReciepents()
      }
    )

    if(!this.lastchange){
      debug('creating lastchange')
      this.lastchange = newLastchange
    }
    else {
      debug('replacing lastchange')
      this.lastchange = newLastchange
    }

  }

  async assertIsTrusted(){}

  async assertIsMetaTrusted(){
    /*
      - is the verification signer an owner?
      - is the verficiation signer a writer?
    */
  }
}

module.exports = File