api/emulations/nfs.js

// Copyright 2018 MaidSafe.net limited.
//
// This SAFE Network Software is licensed to you under
// the MIT license <LICENSE-MIT or http://opensource.org/licenses/MIT> or
// the Modified BSD license <LICENSE-BSD or https://opensource.org/licenses/BSD-3-Clause>,
// at your option.
//
// This file may not be copied, modified, or distributed except according to those terms.
//
// Please review the Licences for the specific language governing permissions and limitations
// relating to use of the SAFE Network Software.


const lib = require('../../native/lib');
const t = require('../../native/types');
const nativeH = require('../../native/helpers');
const { pubConsts: CONSTANTS } = require('../../consts');
const errConst = require('../../error_const');
const makeError = require('../../native/_error.js');

/**
 * NFS-style file operations
 */
class File {

  /**
  * @hideconstructor
  * Instantiate a new NFS File instance.
  *
  * @param {Object} ref the file's metadata including the XoR-name
  * of ImmutableData containing the file's content.
  */
  constructor(ref, connection, fileCtx) {
    this._ref = ref;
    this._fileCtx = fileCtx;
    this._connection = connection;
  }

  /**
  * @private
  * Return an instance of the underlying File structure used by the safe_app
  * lib containing the file's metadata.
  */
  get ref() {
    const data = {
      size: this._ref.size,
      created_sec: this._ref.created_sec,
      created_nsec: this._ref.created_nsec,
      modified_sec: this._ref.modified_sec,
      modified_nsec: this._ref.modified_nsec,
      data_map_name: this.dataMapName,
      user_metadata_ptr: this._ref.user_metadata_ptr,
      user_metadata_len: this._ref.user_metadata_len,
      user_metadata_cap: this._ref.user_metadata_cap
    };

    return new t.File(data);
  }

  /**
   * Get XOR address of file's underlying {@link ImmutableData} data map
   * @returns {Buffer} XOR address
   */
  get dataMapName() {
    return this._ref.data_map_name;
  }

  /**
  * Get metadata passed during file insertion of update
  * @returns {Buffer} user_metadata
  */
  get userMetadata() {
    return this._ref.user_metadata_ptr;
  }

  /**
   * Get UTC date of file context creation
   * @return {Date}
   */
  get created() {
    return nativeH.fromSafeLibTime(this._ref.created_sec, this._ref.created_nsec);
  }

  /**
   * Get UTC date of file context modification
   * @return {Date}
   */
  get modified() {
    return nativeH.fromSafeLibTime(this._ref.modified_sec, this._ref.modified_nsec);
  }

  /**
   * Get file size
   * @returns {Promise<Number>}
   * @example
   * // Assumes {@link MutableData} interface has been obtained
   * const asyncFn = async () => {
   *   try {
   *     const nfs = await mData.emulateAs('NFS');
   *     const fileContext = await nfs.create('<buffer or string>');
   *     const fileSize = await fileContext.size();
   *   } catch (err) {
   *     throw err;
   *   }
   * };
   */
  size() {
    if (!this._fileCtx) {
      return Promise.resolve(this._ref.size);
    }
    return lib.file_size(this._connection, this._fileCtx)
      .then((size) => {
        this._ref.size = size;
        return size;
      });
  }

  /**
   * Read the file.
   * CONSTANTS.NFS_FILE_START and CONSTANTS.NFS_FILE_END may be used
   * to read the entire content of the file. These constants are
   * exposed by the safe-app-nodejs package.
   * @param {Number|CONSTANTS.NFS_FILE_START} position
   * @param {Number|CONSTANTS.NFS_FILE_END} len
   * @throws {ERR_FILE_NOT_FOUND}
   * @returns {Promise<{Buffer, Number}>}
   * @example
   * // Assumes {@link MutableData} interface has been obtained
   * const position = safe.CONSTANTS.NFS_FILE_START;
   * const len = safe.CONSTANTS.NFS_FILE_END;
   * const openMode = safe.CONSTANTS.NFS_FILE_MODE_READ;
   * const asyncFn = async () => {
   *   try {
   *     const nfs = await mData.emulateAs('NFS');
   *     let fileContext = await nfs.create('<buffer or string>');
   *     fileContext = await nfs.open(fileContext, openMode);
   *     const data = await fileContext.read(position, len);
   *   } catch (err) {
   *     throw err;
   *   }
   * };
   */
  read(position, len) {
    if (!this._fileCtx) {
      return Promise
        .reject(makeError(errConst.ERR_FILE_NOT_FOUND.code, errConst.ERR_FILE_NOT_FOUND.msg));
    }
    return lib.file_read(this._connection, this._fileCtx, position, len);
  }

  /**
   * Write file. Does not commit file to network.
   * @param {Buffer|String} content
   * @throws {ERR_FILE_NOT_FOUND}
   * @returns {Promise}
   * @example
   * // Assumes {@link MutableData} interface has been obtained
   * const asyncFn = async () => {
   *     try {
   *         const nfs = await mData.emulateAs('NFS');
   *         const fileContext = await nfs.open();
   *         await fileContext.write('<buffer or string>');
   *     } catch (err) {
   *         throw err;
   *     }
   * };
   */
  write(content) {
    if (!this._fileCtx) {
      return Promise
        .reject(makeError(errConst.ERR_FILE_NOT_FOUND.code, errConst.ERR_FILE_NOT_FOUND.msg));
    }
    return lib.file_write(this._connection, this._fileCtx, content);
  }

  /**
   * Close file and commit to network.
   * @throws {ERR_FILE_NOT_FOUND}
   * @returns {Promise}
   * @example
   * // Assumes {@link MutableData} interface has been obtained
   * const content = '<html><body><h1>WebSite</h1></body></html>';
   * const asyncFn = async () => {
   *     try {
   *         const nfs = await mData.emulateAs('NFS');
   *         const fileContext = await nfs.open();
   *         await fileContext.write('<buffer or string>');
   *         await fileContext.close();
   *     } catch (err) {
   *         throw err;
   *     }
   * };
   */
  close() {
    if (!this._fileCtx) {
      return Promise
        .reject(makeError(errConst.ERR_FILE_NOT_FOUND.code, errConst.ERR_FILE_NOT_FOUND.msg));
    }

    const version = this._ref.version;
    return lib.file_close(this._connection, this._fileCtx)
      .then((res) => {
        this._ref = res;
        this._ref.version = version;
        this._fileCtx = null;
      });
  }

  /**
  * Which version was this? Equals the underlying MutableData's entry version.
  * @return {Number}
  */
  get version() {
    return this._ref.version;
  }

  /**
  * @private
  * Update the file's version. This shall be only internally used and only
  * when its underlying entry in the MutableData is updated
  * @param {Integer} version version to set
  */
  set version(version) {
    this._ref.version = version;
  }
}

/**
* NFS emulation on top of a {@link MutableData}
* @hideconstructor
*/
class NFS {
  constructor(mData) {
    this.mData = mData;
  }

  /**
   * Helper function to create and save file to the network
   * @param {String|Buffer} content
   * @returns {Promise<File>} a newly created file
   * @example
   * // Assumes {@link MutableData} interface has been obtained
   * const content = '<html><body><h1>WebSite</h1></body></html>';
   * const asyncFn = async () => {
   *     try {
   *       const nfs = await mData.emulateAs('NFS');
   *       const fileContext = await nfs.create(content);
   *     } catch(err) {
   *       throw err;
   *     }
   * };
   */
  create(content) {
    return this.open(null, CONSTANTS.NFS_FILE_MODE_OVERWRITE)
      .then((file) => file.write(content)
        .then(() => file.close())
        .then(() => file)
      );
  }

  /**
   * Find the file of the given filename (aka keyName in the MutableData)
   * @param {String} fileName - the path/file name
   * @returns {Promise<File>} - the file found for that path
   * @example
   * // Assumes {@link MutableData} interface has been obtained
   * const content = '<html><body><h1>WebSite</h1></body></html>';
   * const asyncFn = async () => {
   *     const fileName = 'index.html';
   *     try {
   *       const nfs = await mData.emulateAs('NFS');
   *       const fileContext = await nfs.create(content);
   *       await nfs.insert(fileName, fileContext);
   *       const fileContext = await nfs.fetch(fileName);
   *     } catch(err) {
   *       throw err;
   *     }
   * };
   */
  fetch(fileName) {
    return lib.dir_fetch_file(this.mData.app.connection, this.mData.ref, fileName)
      .then((res) => new File(res, this.mData.app.connection, null));
  }

  /**
   * Insert the given file into the underlying {@link MutableData}, directly commit
   * to the network.
   *
   * _Note_: As this application layer, the network does not check any
   * of the metadata provided.
   * @param {(String|Buffer)} fileName The path to store the file under
   * @param {File} file The file to serialise and store
   * @param {String|Buffer} userMetadata
   * @returns {Promise<File>} The same file
   * @example
   * // Assumes {@link MutableData} interface has been obtained
   * const content = '<html><body><h1>WebSite</h1></body></html>';
   * const userMetadata = 'text/html';
   * const asyncFn = async () => {
   *     try {
   *       const nfs = await mData.emulateAs('NFS');
   *       let fileContext = await nfs.create(content);
   *       const fileName = 'index.html';
   *       fileContext = await nfs.insert(fileName, fileContext, userMetadata);
   *     } catch(err) {
   *       throw err;
   *     }
   * };
   */
  insert(fileName, file, userMetadata) {
    if (userMetadata) {
      const userMetadataPtr = Buffer.from(userMetadata);
      const fileMeta = file._ref; // eslint-disable-line no-underscore-dangle
      fileMeta.user_metadata_ptr = userMetadataPtr;
      fileMeta.user_metadata_len = userMetadata.length;
      fileMeta.user_metadata_cap = userMetadataPtr.length;
    }
    return lib.dir_insert_file(
        this.mData.app.connection, this.mData.ref, fileName, file.ref.ref()
      )
      .then(() => {
        const fileObj = file;
        fileObj.version = 0;
        return fileObj;
      });
  }

  /**
   * Replace a path with a new file. Directly commit to the network.
   *
   * CONSTANTS.GET_NEXT_VERSION: Applies update to next file version.
   *
   * _Note_: As this application layer, the network does not check any
   * of the metadata provided.
   * @param {(String|Buffer)} fileName - the path to store the file under
   * @param {File} file - the file to serialise and store
   * @param {Number|CONSTANTS.GET_NEXT_VERSION} version - the version successor number
   * @param {String|Buffer} userMetadata - optional parameter for updating user metadata
   * @returns {Promise<File>} - the same file
   * @example
   * // Assumes {@link MutableData} interface has been obtained
   * const content = '<html><body><h1>Updated WebSite</h1></body></html>';
   * const userMetadata = 'text/html';
   * const asyncFn = async () => {
   *     try {
   *       const version = safe.CONSTANTS.GET_NEXT_VERSION;
   *       const nfs = await mData.emulateAs('NFS');
   *       const fileContext = await nfs.create(content);
   *       const fileName = 'index.html';
   *       fileContext = await nfs.update(fileName, fileContext, version + 1, userMetadata);
   *     } catch(err) {
   *       throw err;
   *     }
   * };
   */
  update(fileName, file, version, userMetadata) {
    if (userMetadata) {
      const userMetadataPtr = Buffer.from(userMetadata);
      const fileMeta = file._ref; // eslint-disable-line no-underscore-dangle
      fileMeta.user_metadata_ptr = userMetadataPtr;
      fileMeta.user_metadata_len = userMetadata.length;
      fileMeta.user_metadata_cap = userMetadataPtr.length;
    }
    const fileContext = file;
    return lib.dir_update_file(this.mData.app.connection, this.mData.ref, fileName,
                           fileContext.ref.ref(), version)
      .then((newVersion) => {
        fileContext.version = newVersion;
      })
      .then(() => fileContext);
  }

  /**
   * Delete a file from path. Directly commit to the network.
   * @param {(String|Buffer)} fileName
   * @param {Number|CONSTANTS.GET_NEXT_VERSION} version - the version successor number
   * @returns {Promise<Number>} - version of deleted file
   * @example
   * // Assumes {@link MutableData} interface has been obtained
   * const content = '<html><body><h1>Updated WebSite</h1></body></html>';
   * const fileName = 'index.html';
   * const asyncFn = async () => {
   *     try {
   *       const version = await mData.getVersion();
   *       const nfs = await mData.emulateAs('NFS');
   *       const fileContext = await nfs.create(content);
   *       fileContext = await nfs.insert(fileName, fileContext);
   *       const version = await nfs.delete(fileName, version + 1);
   *     } catch(err) {
   *       throw err;
   *     }
   * };
   */
  delete(fileName, version) {
    return lib.dir_delete_file(this.mData.app.connection, this.mData.ref, fileName, version)
      .then((newVersion) => newVersion);
  }

  /**
   * Open a file for reading or writing.
   *
   * Open modes (these constants are exported by the safe-app-nodejs package):
   *
   * CONSTANTS.NFS_FILE_MODE_OVERWRITE: Replaces the entire content of the file when writing data.
   *
   * CONSTANTS.NFS_FILE_MODE_APPEND: Appends to existing data in the file.
   *
   * CONSTANTS.NFS_FILE_MODE_READ: Open file to read.
   *
   * @param {File|null} file If no {@link File} is passed,
   * then a new instance is created in {@link CONSTANTS.NFS_FILE_MODE_OVERWRITE}
   * @param {Number|CONSTANTS.NFS_FILE_MODE_OVERWRITE|
   *         CONSTANTS.NFS_FILE_MODE_APPEND|
   *         CONSTANTS.NFS_FILE_MODE_READ} [openMode=CONSTANTS.NFS_FILE_MODE_OVERWRITE]
   * @returns {Promise<File>}
   * @example
   * // Assumes {@link MutableData} interface has been obtained
   * const asyncFn = async () => {
   *     try {
   *       const nfs = await mData.emulateAs('NFS');
   *       const fileContext = await nfs.open();
   *     } catch(err) {
   *       throw err;
   *     }
   * };
   */
  open(file, openMode) {
    const now = nativeH.toSafeLibTime(new Date());
    const metadata = {
      size: 0,
      data_map_name: new Array(32).fill(0),
      created_sec: now.now_sec_part,
      created_nsec: now.now_nsec_part,
      modified_sec: now.now_sec_part,
      modified_nsec: now.now_nsec_part,
      user_metadata_ptr: Buffer.from([]),
      user_metadata_len: 0,
      user_metadata_cap: 0
    };

    let fileParam = file;
    let mode = openMode;
    // FIXME: this is temporary as we should be able to pass a null file to the lib
    if (!file) {
      fileParam = new File(metadata, null, null);
      mode = CONSTANTS.NFS_FILE_MODE_OVERWRITE;
    }

    // FIXME: free/discard the file it's already open, we are missing
    // a function from the lib to perform this.
    return lib.file_open(this.mData.app.connection, this.mData.ref, fileParam.ref.ref(), mode)
      .then((fileCtx) => new File(fileParam.ref, this.mData.app.connection, fileCtx));
  }
}

module.exports = NFS;