Skip to content

Commit

Permalink
feat: 🎸 implement .write() for FSA
Browse files Browse the repository at this point in the history
  • Loading branch information
streamich committed Jun 20, 2023
1 parent e4ce369 commit 8226541
Show file tree
Hide file tree
Showing 7 changed files with 161 additions and 61 deletions.
23 changes: 23 additions & 0 deletions src/consts/FLAG.ts
@@ -0,0 +1,23 @@
// Constants used in `open` system calls, see [open(2)](http://man7.org/linux/man-pages/man2/open.2.html).
export const enum FLAG {
O_RDONLY = 0,
O_WRONLY = 1,
O_RDWR = 2,
O_ACCMODE = 3,
O_CREAT = 64,
O_EXCL = 128,
O_NOCTTY = 256,
O_TRUNC = 512,
O_APPEND = 1024,
O_NONBLOCK = 2048,
O_DSYNC = 4096,
FASYNC = 8192,
O_DIRECT = 16384,
O_LARGEFILE = 0,
O_DIRECTORY = 65536,
O_NOFOLLOW = 131072,
O_NOATIME = 262144,
O_CLOEXEC = 524288,
O_SYNC = 1052672,
O_NDELAY = 2048,
}
19 changes: 14 additions & 5 deletions src/fsa-to-node/FsaNodeFs.ts
Expand Up @@ -14,6 +14,7 @@ import {
dataToBuffer,
flagsToNumber,
genRndStr6,
getWriteArgs,
isFd,
isWin,
modeToNumber,
Expand All @@ -29,6 +30,7 @@ import { FsaToNodeConstants } from './constants';
import { bufferToEncoding } from '../volume';
import { FsaNodeFsOpenFile } from './FsaNodeFsOpenFile';
import { FsaNodeDirent } from './FsaNodeDirent';
import {FLAG} from '../consts/FLAG';
import type { FsCallbackApi, FsPromisesApi } from '../node/types';
import type * as misc from '../node/types/misc';
import type * as opts from '../node/types/options';
Expand Down Expand Up @@ -78,9 +80,9 @@ export class FsaNodeFs implements FsCallbackApi {
return curr;
}

private async getFile(path: string[], name: string, funcName?: string): Promise<fsa.IFileSystemFileHandle> {
private async getFile(path: string[], name: string, funcName?: string, create?: boolean): Promise<fsa.IFileSystemFileHandle> {
const dir = await this.getDir(path, false, funcName);
const file = await dir.getFileHandle(name, { create: false });
const file = await dir.getFileHandle(name, { create });
return file;
}

Expand Down Expand Up @@ -123,7 +125,8 @@ export class FsaNodeFs implements FsCallbackApi {
const filename = pathToFilename(path);
const flagsNum = flagsToNumber(flags);
const [folder, name] = pathToLocation(filename);
this.getFile(folder, name, 'open')
const createIfMissing = !!(flagsNum & FLAG.O_CREAT);
this.getFile(folder, name, 'open', createIfMissing)
.then(file => {
const fd = this.newFdNumber();
const openFile = new FsaNodeFsOpenFile(fd, modeNum, flagsNum, file);
Expand Down Expand Up @@ -178,8 +181,14 @@ export class FsaNodeFs implements FsCallbackApi {
});
};

public readonly write: FsCallbackApi['write'] = (fd: number, a?, b?, c?, d?, e?) => {
throw new Error('Not implemented');
public readonly write: FsCallbackApi['write'] = (fd: number, a?: unknown, b?: unknown, c?: unknown, d?: unknown, e?: unknown) => {
const [, asStr, buf, offset, length, position, cb] = getWriteArgs(fd, a, b, c, d, e);
(async () => {
const openFile = await this.getFileByFd(fd, 'write');
const data = buf.subarray(offset, offset + length);
await openFile.write(data, position);
return length;
})().then((bytesWritten) => cb(null, bytesWritten, asStr ? a : buf), (error) => cb(error));
};

writeFile(id: misc.TFileId, data: misc.TData, callback: misc.TCallback<void>);
Expand Down
23 changes: 23 additions & 0 deletions src/fsa-to-node/FsaNodeFsOpenFile.ts
Expand Up @@ -2,6 +2,16 @@ import type * as fsa from '../fsa/types';
import type * as misc from '../node/types/misc';

export class FsaNodeFsOpenFile {
protected seek: number = 0;

/**
* This influences the behavior of the next write operation. On the first
* write we want to overwrite the file or keep the existing data, depending
* with which flags the file was opened. On subsequent writes we want to
* append to the file.
*/
protected keepExistingData: boolean = false;

public constructor(
public readonly fd: number,
public readonly mode: misc.TMode,
Expand All @@ -10,4 +20,17 @@ export class FsaNodeFsOpenFile {
) {}

public async close(): Promise<void> {}

public async write(data: Uint8Array, seek: number | null): Promise<void> {
if (typeof seek !== 'number') seek = this.seek;
const writer = await this.file.createWritable({keepExistingData: this.keepExistingData});
await writer.write({
type: 'write',
data,
position: seek,
});
await writer.close();
this.keepExistingData = true;
this.seek += data.length;
}
}
44 changes: 44 additions & 0 deletions src/fsa-to-node/__tests__/FsaNodeFs.test.ts
Expand Up @@ -307,3 +307,47 @@ describe('.appendFile()', () => {
expect(mfs.readFileSync('/mountpoint/file', 'utf8')).toBe('123x');
});
});

describe('.write()', () => {
test('can write to a file', async () => {
const { fs, mfs } = setup({});
const fd = await new Promise<number>((resolve, reject) => fs.open('/test.txt', 'w', (err, fd) => {
if (err) reject(err);
else resolve(fd!);
}));
const [bytesWritten, data] = await new Promise<[number, any]>((resolve, reject) => {
fs.write(fd, 'a', (err, bytesWritten, data) => {
if (err) reject(err);
else resolve([bytesWritten, data]);
});
});
expect(bytesWritten).toBe(1);
expect(data).toBe('a');
expect(mfs.readFileSync('/mountpoint/test.txt', 'utf8')).toBe('a');
});

test('can write to a file twice sequentially', async () => {
const { fs, mfs } = setup({});
const fd = await new Promise<number>((resolve, reject) => fs.open('/test.txt', 'w', (err, fd) => {
if (err) reject(err);
else resolve(fd!);
}));
const res1 = await new Promise<[number, any]>((resolve, reject) => {
fs.write(fd, 'a', (err, bytesWritten, data) => {
if (err) reject(err);
else resolve([bytesWritten, data]);
});
});
expect(res1[0]).toBe(1);
expect(res1[1]).toBe('a');
const res2 = await new Promise<[number, any]>((resolve, reject) => {
fs.write(fd, 'bc', (err, bytesWritten, data) => {
if (err) reject(err);
else resolve([bytesWritten, data]);
});
});
expect(res2[0]).toBe(2);
expect(res2[1]).toBe('bc');
expect(mfs.readFileSync('/mountpoint/test.txt', 'utf8')).toBe('abc');
});
});
2 changes: 1 addition & 1 deletion src/node.ts
Expand Up @@ -531,7 +531,7 @@ export class File {
return Stats.build(this.node) as Stats<number>;
}

write(buf: Buffer, offset: number = 0, length: number = buf.length, position?: number): number {
write(buf: Buffer, offset: number = 0, length: number = buf.length, position?: number | null): number {
if (typeof position !== 'number') position = this.position;
const bytes = this.node.write(buf, offset, length, position);
this.position = position + bytes;
Expand Down
52 changes: 52 additions & 0 deletions src/node/util.ts
Expand Up @@ -176,3 +176,55 @@ export function dataToBuffer(data: misc.TData, encoding: string = ENCODING_UTF8)
else if (data instanceof Uint8Array) return bufferFrom(data);
else return bufferFrom(String(data), encoding);
}

export const getWriteArgs =
(fd: number, a?: unknown, b?: unknown, c?: unknown, d?: unknown, e?: unknown):
[fd: number, dataAsStr: boolean, buf: Buffer, offset: number, length: number, position: number | null, callback: (...args) => void] => {
validateFd(fd);
let offset: number = 0;
let length: number | undefined;
let position: number | null = null;
let encoding: BufferEncoding | undefined;
let callback: ((...args) => void) | undefined;
const tipa = typeof a;
const tipb = typeof b;
const tipc = typeof c;
const tipd = typeof d;
if (tipa !== 'string') {
if (tipb === 'function') {
callback = <(...args) => void>b;
} else if (tipc === 'function') {
offset = <number>b | 0;
callback = <(...args) => void>c;
} else if (tipd === 'function') {
offset = <number>b | 0;
length = <number>c;
callback = <(...args) => void>d;
} else {
offset = <number>b | 0;
length = <number>c;
position = <number | null>d;
callback = <(...args) => void>e;
}
} else {
if (tipb === 'function') {
callback = <(...args) => void>b;
} else if (tipc === 'function') {
position = <number | null>b;
callback = <(...args) => void>c;
} else if (tipd === 'function') {
position = <number | null>b;
encoding = <BufferEncoding>c;
callback = <(...args) => void>d;
}
}
const buf: Buffer = dataToBuffer(<string | Buffer>a, encoding);
if (tipa !== 'string') {
if (typeof length === 'undefined') length = buf.length;
} else {
offset = 0;
length = buf.length;
}
const cb = validateCallback(callback);
return [fd, tipa === 'string', buf, offset, length!, position, cb];
};
59 changes: 4 additions & 55 deletions src/volume.ts
Expand Up @@ -42,6 +42,7 @@ import {
isFd,
isWin,
dataToBuffer,
getWriteArgs,
} from './node/util';
import type { PathLike, symlink } from 'fs';

Expand Down Expand Up @@ -914,7 +915,7 @@ export class Volume {
this.wrapAsync(this.readFileBase, [id, flagsNum, opts.encoding], callback);
}

private writeBase(fd: number, buf: Buffer, offset?: number, length?: number, position?: number): number {
private writeBase(fd: number, buf: Buffer, offset?: number, length?: number, position?: number | null): number {
const file = this.getFileByFdOrThrow(fd, 'write');
return file.write(buf, offset, length, position);
}
Expand Down Expand Up @@ -986,63 +987,11 @@ export class Volume {
write(fd: number, str: string, position: number, callback: (...args) => void);
write(fd: number, str: string, position: number, encoding: BufferEncoding, callback: (...args) => void);
write(fd: number, a?, b?, c?, d?, e?) {
validateFd(fd);

let offset: number;
let length: number | undefined;
let position: number;
let encoding: BufferEncoding | undefined;
let callback: ((...args) => void) | undefined;

const tipa = typeof a;
const tipb = typeof b;
const tipc = typeof c;
const tipd = typeof d;

if (tipa !== 'string') {
if (tipb === 'function') {
callback = b;
} else if (tipc === 'function') {
offset = b | 0;
callback = c;
} else if (tipd === 'function') {
offset = b | 0;
length = c;
callback = d;
} else {
offset = b | 0;
length = c;
position = d;
callback = e;
}
} else {
if (tipb === 'function') {
callback = b;
} else if (tipc === 'function') {
position = b;
callback = c;
} else if (tipd === 'function') {
position = b;
encoding = c;
callback = d;
}
}

const buf: Buffer = dataToBuffer(a, encoding);

if (tipa !== 'string') {
if (typeof length === 'undefined') length = buf.length;
} else {
offset = 0;
length = buf.length;
}

const cb = validateCallback(callback);

const [, asStr, buf, offset, length, position, cb] = getWriteArgs(fd, a, b, c, d, e);
setImmediate(() => {
try {
const bytes = this.writeBase(fd, buf, offset, length, position);
if (tipa !== 'string') {
if (!asStr) {
cb(null, bytes, buf);
} else {
cb(null, bytes, a);
Expand Down

0 comments on commit 8226541

Please sign in to comment.