Skip to content

Implement FileHandle API #1089

@streamich

Description

@streamich

Below is Node.js implementation of FileHandle, for reference. We need to implement a similar functionality for in-memory memfs library.

class FileHandle extends EventEmitter {
  /**
   * @param {InternalFSBinding.FileHandle | undefined} filehandle
   */
  constructor(filehandle) {
    super();
    markTransferMode(this, false, true);
    this[kHandle] = filehandle;
    this[kFd] = filehandle ? filehandle.fd : -1;

    this[kRefs] = 1;
    this[kClosePromise] = null;
  }

  getAsyncId() {
    return this[kHandle].getAsyncId();
  }

  get fd() {
    return this[kFd];
  }

  appendFile(data, options) {
    return fsCall(writeFile, this, data, options);
  }

  chmod(mode) {
    return fsCall(fchmod, this, mode);
  }

  chown(uid, gid) {
    return fsCall(fchown, this, uid, gid);
  }

  datasync() {
    return fsCall(fdatasync, this);
  }

  sync() {
    return fsCall(fsync, this);
  }

  read(buffer, offset, length, position) {
    return fsCall(read, this, buffer, offset, length, position);
  }

  readv(buffers, position) {
    return fsCall(readv, this, buffers, position);
  }

  readFile(options) {
    return fsCall(readFile, this, options);
  }

  readLines(options = undefined) {
    return new Interface({
      input: this.createReadStream(options),
      crlfDelay: Infinity,
    });
  }

  stat(options) {
    return fsCall(fstat, this, options);
  }

  truncate(len = 0) {
    return fsCall(ftruncate, this, len);
  }

  utimes(atime, mtime) {
    return fsCall(futimes, this, atime, mtime);
  }

  write(buffer, offset, length, position) {
    return fsCall(write, this, buffer, offset, length, position);
  }

  writev(buffers, position) {
    return fsCall(writev, this, buffers, position);
  }

  writeFile(data, options) {
    return fsCall(writeFile, this, data, options);
  }

  close = () => {
    if (this[kFd] === -1) {
      return PromiseResolve();
    }

    if (this[kClosePromise]) {
      return this[kClosePromise];
    }

    this[kRefs]--;
    if (this[kRefs] === 0) {
      this[kFd] = -1;
      this[kClosePromise] = SafePromisePrototypeFinally(
        this[kHandle].close(),
        () => { this[kClosePromise] = undefined; },
      );
    } else {
      this[kClosePromise] = SafePromisePrototypeFinally(
        new Promise((resolve, reject) => {
          this[kCloseResolve] = resolve;
          this[kCloseReject] = reject;
        }), () => {
          this[kClosePromise] = undefined;
          this[kCloseReject] = undefined;
          this[kCloseResolve] = undefined;
        },
      );
    }

    this.emit('close');
    return this[kClosePromise];
  };

  async [SymbolAsyncDispose]() {
    await this.close();
  }

  /**
   * @typedef {import('../webstreams/readablestream').ReadableStream
   * } ReadableStream
   * @param {{ type?: 'bytes', autoClose?: boolean }} [options]
   * @returns {ReadableStream}
   */
  readableWebStream(options = kEmptyObject) {
    if (this[kFd] === -1)
      throw new ERR_INVALID_STATE('The FileHandle is closed');
    if (this[kClosePromise])
      throw new ERR_INVALID_STATE('The FileHandle is closing');
    if (this[kLocked])
      throw new ERR_INVALID_STATE('The FileHandle is locked');
    this[kLocked] = true;

    validateObject(options, 'options');
    const {
      type = 'bytes',
      autoClose = false,
    } = options;

    validateBoolean(autoClose, 'options.autoClose');

    if (type !== 'bytes') {
      process.emitWarning(
        'A non-"bytes" options.type has no effect. A byte-oriented steam is ' +
        'always created.',
        'ExperimentalWarning',
      );
    }

    const readFn = FunctionPrototypeBind(this.read, this);
    const ondone = async () => {
      this[kUnref]();
      if (autoClose) await this.close();
    };

    const ReadableStream = lazyReadableStream();
    const readable = new ReadableStream({
      type: 'bytes',
      autoAllocateChunkSize: 16384,

      async pull(controller) {
        const view = controller.byobRequest.view;
        const { bytesRead } = await readFn(view, view.byteOffset, view.byteLength);

        if (bytesRead === 0) {
          controller.close();
          await ondone();
        }

        controller.byobRequest.respond(bytesRead);
      },

      async cancel() {
        await ondone();
      },
    });


    const {
      readableStreamCancel,
    } = require('internal/webstreams/readablestream');
    this[kRef]();
    this.once('close', () => {
      readableStreamCancel(readable);
    });

    return readable;
  }

  /**
   * @typedef {import('./streams').ReadStream
   * } ReadStream
   * @param {{
   *   encoding?: string;
   *   autoClose?: boolean;
   *   emitClose?: boolean;
   *   start: number;
   *   end?: number;
   *   highWaterMark?: number;
   *   }} [options]
   * @returns {ReadStream}
   */
  createReadStream(options = undefined) {
    const { ReadStream } = lazyFsStreams();
    return new ReadStream(undefined, { ...options, fd: this });
  }

  /**
   * @typedef {import('./streams').WriteStream
   * } WriteStream
   * @param {{
   *   encoding?: string;
   *   autoClose?: boolean;
   *   emitClose?: boolean;
   *   start: number;
   *   highWaterMark?: number;
   *   flush?: boolean;
   *   }} [options]
   * @returns {WriteStream}
   */
  createWriteStream(options = undefined) {
    const { WriteStream } = lazyFsStreams();
    return new WriteStream(undefined, { ...options, fd: this });
  }

  [kTransfer]() {
    if (this[kClosePromise] || this[kRefs] > 1) {
      throw lazyDOMException('Cannot transfer FileHandle while in use',
                             'DataCloneError');
    }

    const handle = this[kHandle];
    this[kFd] = -1;
    this[kHandle] = null;
    this[kRefs] = 0;

    return {
      data: { handle },
      deserializeInfo: 'internal/fs/promises:FileHandle',
    };
  }

  [kTransferList]() {
    return [ this[kHandle] ];
  }

  [kDeserialize]({ handle }) {
    this[kHandle] = handle;
    this[kFd] = handle.fd;
  }

  [kRef]() {
    this[kRefs]++;
  }

  [kUnref]() {
    this[kRefs]--;
    if (this[kRefs] === 0) {
      this[kFd] = -1;
      PromisePrototypeThen(
        this[kHandle].close(),
        this[kCloseResolve],
        this[kCloseReject],
      );
    }
  }
}

Metadata

Metadata

Assignees

Labels

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions