diff --git a/src/consts/FLAG.ts b/src/consts/FLAG.ts new file mode 100644 index 000000000..e5cf28aed --- /dev/null +++ b/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, +} diff --git a/src/fsa-to-node/FsaNodeFs.ts b/src/fsa-to-node/FsaNodeFs.ts index e056abfd2..304c6d526 100644 --- a/src/fsa-to-node/FsaNodeFs.ts +++ b/src/fsa-to-node/FsaNodeFs.ts @@ -14,6 +14,7 @@ import { dataToBuffer, flagsToNumber, genRndStr6, + getWriteArgs, isFd, isWin, modeToNumber, @@ -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'; @@ -78,9 +80,9 @@ export class FsaNodeFs implements FsCallbackApi { return curr; } - private async getFile(path: string[], name: string, funcName?: string): Promise { + private async getFile(path: string[], name: string, funcName?: string, create?: boolean): Promise { 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; } @@ -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); @@ -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); diff --git a/src/fsa-to-node/FsaNodeFsOpenFile.ts b/src/fsa-to-node/FsaNodeFsOpenFile.ts index 33a92fbc2..0b9afa631 100644 --- a/src/fsa-to-node/FsaNodeFsOpenFile.ts +++ b/src/fsa-to-node/FsaNodeFsOpenFile.ts @@ -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, @@ -10,4 +20,17 @@ export class FsaNodeFsOpenFile { ) {} public async close(): Promise {} + + public async write(data: Uint8Array, seek: number | null): Promise { + 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; + } } diff --git a/src/fsa-to-node/__tests__/FsaNodeFs.test.ts b/src/fsa-to-node/__tests__/FsaNodeFs.test.ts index 097b08d00..4be53b421 100644 --- a/src/fsa-to-node/__tests__/FsaNodeFs.test.ts +++ b/src/fsa-to-node/__tests__/FsaNodeFs.test.ts @@ -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((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((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'); + }); +}); diff --git a/src/node.ts b/src/node.ts index e0587531c..c1465e32d 100644 --- a/src/node.ts +++ b/src/node.ts @@ -531,7 +531,7 @@ export class File { return Stats.build(this.node) as Stats; } - 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; diff --git a/src/node/util.ts b/src/node/util.ts index fed3e17c2..7d3157846 100644 --- a/src/node/util.ts +++ b/src/node/util.ts @@ -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 = b | 0; + callback = <(...args) => void>c; + } else if (tipd === 'function') { + offset = b | 0; + length = c; + callback = <(...args) => void>d; + } else { + offset = b | 0; + length = c; + position = d; + callback = <(...args) => void>e; + } + } else { + if (tipb === 'function') { + callback = <(...args) => void>b; + } else if (tipc === 'function') { + position = b; + callback = <(...args) => void>c; + } else if (tipd === 'function') { + position = b; + encoding = c; + callback = <(...args) => void>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); + return [fd, tipa === 'string', buf, offset, length!, position, cb]; +}; diff --git a/src/volume.ts b/src/volume.ts index 2c0c4c3af..3ec1e5000 100644 --- a/src/volume.ts +++ b/src/volume.ts @@ -42,6 +42,7 @@ import { isFd, isWin, dataToBuffer, + getWriteArgs, } from './node/util'; import type { PathLike, symlink } from 'fs'; @@ -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); } @@ -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);