diff --git a/src/fsa-to-node/FsaNodeFs.ts b/src/fsa-to-node/FsaNodeFs.ts index db916e278..012a1b61c 100644 --- a/src/fsa-to-node/FsaNodeFs.ts +++ b/src/fsa-to-node/FsaNodeFs.ts @@ -7,9 +7,11 @@ import { getRmOptsAndCb, getRmdirOptions, optsAndCbGenerator, + getAppendFileOptsAndCb, } from '../node/options'; import { createError, + dataToBuffer, flagsToNumber, genRndStr6, isFd, @@ -96,6 +98,14 @@ export class FsaNodeFs implements FsCallbackApi { return await this.getFile(folder, name, funcName); } + private async getFileByIdOrCreate(id: misc.TFileId, funcName?: string): Promise { + if (typeof id === 'number') return (await this.getFileByFd(id, funcName)).file; + const filename = pathToFilename(id); + const [folder, name] = pathToLocation(filename); + const dir = await this.getDir(folder, false, funcName); + return await dir.getFileHandle(name, { create: true }); + } + public readonly open: FsCallbackApi['open'] = ( path: misc.PathLike, flags: misc.TFlags, @@ -280,16 +290,19 @@ export class FsaNodeFs implements FsCallbackApi { throw new Error('Not implemented'); } - appendFile(id: misc.TFileId, data: misc.TData, callback: misc.TCallback); - appendFile( - id: misc.TFileId, - data: misc.TData, - options: opts.IAppendFileOptions | string, - callback: misc.TCallback, - ); - appendFile(id: misc.TFileId, data: misc.TData, a, b?) { - throw new Error('Not implemented'); - } + public readonly appendFile: FsCallbackApi['appendFile'] = (id: misc.TFileId, data: misc.TData, a, b?) => { + const [opts, callback] = getAppendFileOptsAndCb(a, b); + const buffer = dataToBuffer(data, opts.encoding); + this.getFileByIdOrCreate(id, 'appendFile') + .then(file => (async () => { + const blob = await file.getFile(); + const writable = await file.createWritable({ keepExistingData: true }); + await writable.seek(blob.size); + await writable.write(buffer); + await writable.close(); + })()) + .then(() => callback(null), error => callback(error)); + }; public readonly readdir: FsCallbackApi['readdir'] = (path: misc.PathLike, a?, b?) => { const [options, callback] = getReaddirOptsAndCb(a, b); diff --git a/src/fsa-to-node/__tests__/FsaNodeFs.test.ts b/src/fsa-to-node/__tests__/FsaNodeFs.test.ts index 622356094..f803ef19a 100644 --- a/src/fsa-to-node/__tests__/FsaNodeFs.test.ts +++ b/src/fsa-to-node/__tests__/FsaNodeFs.test.ts @@ -286,3 +286,24 @@ describe('.readdir()', () => { expect(list.find(item => item.name === 'f.html')?.isDirectory()).toBe(false); }); }); + +describe('.appendFile()', () => { + test('can create a file', async () => { + const { fs, mfs } = setup({}); + await fs.promises.appendFile('/test.txt', 'a'); + expect(mfs.readFileSync('/mountpoint/test.txt', 'utf8')).toBe('a'); + }); + + test('can append to a file', async () => { + const { fs, mfs } = setup({}); + await fs.promises.appendFile('/test.txt', 'a'); + await fs.promises.appendFile('/test.txt', 'b'); + expect(mfs.readFileSync('/mountpoint/test.txt', 'utf8')).toBe('ab'); + }); + + test('can append to a file - 2', async () => { + const { fs, mfs } = setup({ file: '123'}); + await fs.promises.appendFile('file', 'x'); + expect(mfs.readFileSync('/mountpoint/file', 'utf8')).toBe('123x'); + }); +}); diff --git a/src/node/options.ts b/src/node/options.ts index 1d9a347d7..7c6fb902a 100644 --- a/src/node/options.ts +++ b/src/node/options.ts @@ -1,8 +1,9 @@ import type * as opts from './types/options'; -import { MODE } from './constants'; +import { FLAGS, MODE } from './constants'; import { assertEncoding } from '../encoding'; import * as misc from './types/misc'; import { validateCallback } from './util'; +import {IAppendFileOptions} from '../volume'; const mkdirDefaults: opts.IMkdirOptions = { mode: MODE.DIR, @@ -78,3 +79,11 @@ export const getReaddirOptions = optsGenerator(readdirDefa export const getReaddirOptsAndCb = optsAndCbGenerator( getReaddirOptions, ); + +const appendFileDefaults: opts.IAppendFileOptions = { + encoding: 'utf8', + mode: MODE.DEFAULT, + flag: FLAGS[FLAGS.a], +}; +export const getAppendFileOpts = optsGenerator(appendFileDefaults); +export const getAppendFileOptsAndCb = optsAndCbGenerator(getAppendFileOpts); diff --git a/src/node/util.ts b/src/node/util.ts index f37e24d38..79028f11a 100644 --- a/src/node/util.ts +++ b/src/node/util.ts @@ -2,6 +2,8 @@ import { ERRSTR, FLAGS } from './constants'; import * as errors from '../internal/errors'; import type { FsCallbackApi } from './types'; import type * as misc from './types/misc'; +import {ENCODING_UTF8} from '../encoding'; +import {bufferFrom} from '../internal/buffer'; export const isWin = process.platform === 'win32'; @@ -168,3 +170,9 @@ export function isFd(path): boolean { export function validateFd(fd) { if (!isFd(fd)) throw TypeError(ERRSTR.FD); } + +export function dataToBuffer(data: misc.TData, encoding: string = ENCODING_UTF8): Buffer { + if (Buffer.isBuffer(data)) return data; + else if (data instanceof Uint8Array) return bufferFrom(data); + else return bufferFrom(String(data), encoding); +} diff --git a/src/volume.ts b/src/volume.ts index d8e9f97d7..2c0c4c3af 100644 --- a/src/volume.ts +++ b/src/volume.ts @@ -27,6 +27,8 @@ import { optsAndCbGenerator, optsDefaults, optsGenerator, + getAppendFileOptsAndCb, + getAppendFileOpts, } from './node/options'; import { validateCallback, @@ -39,6 +41,7 @@ import { validateFd, isFd, isWin, + dataToBuffer, } from './node/util'; import type { PathLike, symlink } from 'fs'; @@ -121,13 +124,6 @@ const getWriteFileOptions = optsGenerator(writeFileDefaults); // Options for `fs.appendFile` and `fs.appendFileSync` export interface IAppendFileOptions extends opts.IFileOptions {} -const appendFileDefaults: IAppendFileOptions = { - encoding: 'utf8', - mode: MODE.DEFAULT, - flag: FLAGS[FLAGS.a], -}; -const getAppendFileOpts = optsGenerator(appendFileDefaults); -const getAppendFileOptsAndCb = optsAndCbGenerator(getAppendFileOpts); // Options for `fs.realpath` and `fs.realpathSync` export interface IRealpathOptions { @@ -229,12 +225,6 @@ export function dataToStr(data: TData, encoding: string = ENCODING_UTF8): string else return String(data); } -export function dataToBuffer(data: TData, encoding: string = ENCODING_UTF8): Buffer { - if (Buffer.isBuffer(data)) return data; - else if (data instanceof Uint8Array) return bufferFrom(data); - else return bufferFrom(String(data), encoding); -} - export function bufferToEncoding(buffer: Buffer, encoding?: TEncodingExtended): TDataOut { if (!encoding || encoding === 'buffer') return buffer; else return buffer.toString(encoding); @@ -1475,7 +1465,7 @@ export class Volume { this.wrapAsync(this.accessBase, [filename, mode], callback); } - appendFileSync(id: TFileId, data: TData, options: IAppendFileOptions | string = appendFileDefaults) { + appendFileSync(id: TFileId, data: TData, options?: IAppendFileOptions | string) { const opts = getAppendFileOpts(options); // force append behavior when using a supplied file descriptor