Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Want a More Modern Version of Your Code? #39

Open
machineghost opened this issue Oct 24, 2021 · 2 comments
Open

Want a More Modern Version of Your Code? #39

machineghost opened this issue Oct 24, 2021 · 2 comments

Comments

@machineghost
Copy link

machineghost commented Oct 24, 2021

I stole your source code, and then revamped it to use modern JS (eg. async/await). I also changed your snake case variables to camel case, factored a few inner functions outside, etc. (I also eliminated the encoding arg since I didn't need it, but it could easily be restored and I left a note about it in the comments.)

Now I completely understand that when someone says "hey I took your great source code and screwed it all up", the original author runs screaming in the opposite :) So feel free to close this ticket without even looking at it ... but just in case you wanted to see my take, I thought I'd share.

Either way though, thanks for making this (small but useful) library :)

const readPreviousChar = async (stat, file, currentCharacterCount) => {
  const position = stat.size - 1 - currentCharacterCount;
  const { bytesRead, buffer } = await fs.read(
    file, // file descriptor
    Buffer.alloc(1), // buffer to write data to
    0, // offset
    1, // # of bytes to read
    position // position in data to read
  );
  return String.fromCharCode(buffer[0]);
};

const NEW_LINE_CHARACTERS = ['\n'];

const doWhileLoop = async function (
  self,
  lines,
  chars,
  lineCount,
  maxLineCount,
  resolve
) {
  if (lines.length > self.stat.size) {
    lines = lines.substring(lines.length - self.stat.size);
  }

  if (lines.length >= self.stat.size || lineCount >= maxLineCount) {
    if (NEW_LINE_CHARACTERS.includes(lines.substring(0, 1))) {
      lines = lines.substring(1);
    }
    fs.close(self.file);
    // I didn't have to worry about other encoding types;
    // if you want to keep supporting them this function would need an encoding arg
    // if (encoding === 'buffer') {
    //   return resolve(Buffer.from(lines, 'binary'));
    // }
    // return resolve(Buffer.from(lines, 'binary').toString(encoding));
    return resolve(Buffer.from(lines, 'binary').toString('utf8'));
  }

  const nextCharacter = await readPreviousChar(self.stat, self.file, chars);
  lines = nextCharacter + lines;
  if (NEW_LINE_CHARACTERS.includes(nextCharacter) && lines.length > 1) {
    lineCount++;
  }
  chars++;

  await doWhileLoop(self, lines, chars, lineCount, maxLineCount, resolve);
};

/**
 * Read in the last `n` lines of a file
 * @param  {string}   inputFilePath - file (direct or relative path to file.)
 * @param  {int}      maxLineCount    - max number of lines to read in.
 * @param  {encoding} encoding        - specifies the character encoding to be used, or 'buffer'. defaults to 'utf8'.
 *
 * @return {promise}  a promise resolved with the lines or rejected with an error.
 */
const readLast = async (inputFilePath, maxLineCount) => {
  return new Promise(async (resolve, reject) => {
    let self = { stat: null, file: null };

    try {
      // @see https://nodejs.org/api/fs.html#fsexistspath-callback
      // "Using fs.exists() to check for the existence of a file before calling
      // fs.open(), fs.readFile() or fs.writeFile() is not recommended"

      // if (!(await fs.exists(inputFilePath))) {
      //   throw new Error(`file does not exist: ${inputFilePath}`);
      // }

      await Promise.all([
        // Load file Stats.
        fs.stat(inputFilePath).then((stat) => (self.stat = stat)),
        // Open file for reading.
        fs.open(inputFilePath, 'r').then((file) => (self.file = file)),
      ]);
      let chars = 0;
      let lines = '';

      return doWhileLoop(self, lines, chars, 0, maxLineCount, resolve);
    } catch (reason) {
      if (self.file !== null) {
        try {
          fs.close(self.file);
        } catch {
          // We might get here if the encoding is invalid.
          // Since we are already rejecting, let's ignore this error.
        }
      }
      return reject(reason);
    }
  });
};

@tjx666
Copy link

tjx666 commented Mar 15, 2022

Check here: https://github.com/tjx666/scripting-listener/blob/master/src/LogViewer/readLastLines.ts

@cbratschi
Copy link

Well, we used this package for a very long time but the Next.js bundler had some issues with the imports and I had a look at the source code and ended with this rewrite in TypeScript:

import fs from 'node:fs';

/**
 * New line character.
 */
const NEW_LINE_CODE = '\n'.charCodeAt(0);

/**
 * Buffer size to use.
 */
const BUFFER_SIZE = 256;

/**
 * Scans the file from the end for lines.
 *
 * @param file
 * @param maxLineCount
 * @param encoding
 * @returns
 */
async function readLastLinesFromFile(file: fs.promises.FileHandle, maxLineCount: number, encoding?: BufferEncoding): Promise<string> {
    const stat = await file.stat();
    let remainingBytes = stat.size;
    let lineCount = 0;
    const prevBuffers: Buffer[] = [];

    outer: while (remainingBytes > 0) {
        //read a buffer
        const bufferLen = Math.min(BUFFER_SIZE, remainingBytes);
        const position = remainingBytes - bufferLen;
        const { bytesRead, buffer } = await file.read({
            buffer: Buffer.alloc(bufferLen),
            offset: 0,
            length: bufferLen,
            position
        });

        if (bytesRead !== bufferLen) {
            throw new Error('read size mismatch');
        }

        //scan for new line (\n or \r\n)
        for (let i = bufferLen - 1; i >= 0; i--) {
            if (buffer[i] === NEW_LINE_CODE) {
                lineCount++;

                //check end condition
                if (lineCount >= maxLineCount) {
                    prevBuffers.push(buffer.subarray(i + 1));
                    break outer;
                }
            }
        }

        prevBuffers.push(buffer);

        //next
        remainingBytes -= bufferLen;
    }

    return Buffer.concat(prevBuffers.reverse()).toString(encoding);
}

/**
 * Read in the last `n` lines of a file.
 *
 * Code based on https://github.com/alexbbt/read-last-lines/issues/39.
 *
 * @param inputFilePath file (direct or relative path to file.)
 * @param maxLineCount max number of lines to read in.
 * @param encoding
 *
 * @return a promise resolved with the lines or rejected with an error.
 */
export async function readLastLines(inputFilePath: string, maxLineCount: number, encoding?: BufferEncoding): Promise<string> {
    //validate
    if (maxLineCount <= 0) {
        return '';
    }

    //scan
    let file: fs.promises.FileHandle | undefined;

    try {
        //open file for reading
        file = await fs.promises.open(inputFilePath, 'r');

        return await readLastLinesFromFile(file, maxLineCount, encoding);
    } finally {
        await file?.close();
    }
}

Feel free to use this version which has several optimizations.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants