Skip to content

Commit

Permalink
feat: 🎸 implement initial version of appendFile() method
Browse files Browse the repository at this point in the history
  • Loading branch information
streamich committed Jun 20, 2023
1 parent 6f11ec8 commit 616be8d
Show file tree
Hide file tree
Showing 5 changed files with 66 additions and 25 deletions.
33 changes: 23 additions & 10 deletions src/fsa-to-node/FsaNodeFs.ts
Expand Up @@ -7,9 +7,11 @@ import {
getRmOptsAndCb,
getRmdirOptions,
optsAndCbGenerator,
getAppendFileOptsAndCb,
} from '../node/options';
import {
createError,
dataToBuffer,
flagsToNumber,
genRndStr6,
isFd,
Expand Down Expand Up @@ -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<fsa.IFileSystemFileHandle> {
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,
Expand Down Expand Up @@ -280,16 +290,19 @@ export class FsaNodeFs implements FsCallbackApi {
throw new Error('Not implemented');
}

appendFile(id: misc.TFileId, data: misc.TData, callback: misc.TCallback<void>);
appendFile(
id: misc.TFileId,
data: misc.TData,
options: opts.IAppendFileOptions | string,
callback: misc.TCallback<void>,
);
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);
Expand Down
21 changes: 21 additions & 0 deletions src/fsa-to-node/__tests__/FsaNodeFs.test.ts
Expand Up @@ -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');
});
});
11 changes: 10 additions & 1 deletion 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,
Expand Down Expand Up @@ -78,3 +79,11 @@ export const getReaddirOptions = optsGenerator<opts.IReaddirOptions>(readdirDefa
export const getReaddirOptsAndCb = optsAndCbGenerator<opts.IReaddirOptions, misc.TDataOut[] | misc.IDirent[]>(
getReaddirOptions,
);

const appendFileDefaults: opts.IAppendFileOptions = {
encoding: 'utf8',
mode: MODE.DEFAULT,
flag: FLAGS[FLAGS.a],
};
export const getAppendFileOpts = optsGenerator<IAppendFileOptions>(appendFileDefaults);
export const getAppendFileOptsAndCb = optsAndCbGenerator<IAppendFileOptions, void>(getAppendFileOpts);
8 changes: 8 additions & 0 deletions src/node/util.ts
Expand Up @@ -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';

Expand Down Expand Up @@ -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);
}
18 changes: 4 additions & 14 deletions src/volume.ts
Expand Up @@ -27,6 +27,8 @@ import {
optsAndCbGenerator,
optsDefaults,
optsGenerator,
getAppendFileOptsAndCb,
getAppendFileOpts,
} from './node/options';
import {
validateCallback,
Expand All @@ -39,6 +41,7 @@ import {
validateFd,
isFd,
isWin,
dataToBuffer,
} from './node/util';
import type { PathLike, symlink } from 'fs';

Expand Down Expand Up @@ -121,13 +124,6 @@ const getWriteFileOptions = optsGenerator<IWriteFileOptions>(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<IAppendFileOptions>(appendFileDefaults);
const getAppendFileOptsAndCb = optsAndCbGenerator<IAppendFileOptions, void>(getAppendFileOpts);

// Options for `fs.realpath` and `fs.realpathSync`
export interface IRealpathOptions {
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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
Expand Down

0 comments on commit 616be8d

Please sign in to comment.