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

Taking forward the Node fs polyfill #4294

Merged
merged 12 commits into from
Mar 8, 2020
193 changes: 193 additions & 0 deletions std/node/_fs/_fs_appendFile.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license.
import { FileOptions, isFileOptions } from "./_fs_common.ts";
import { notImplemented } from "../_utils.ts";

/**
* TODO: Also accept 'data' parameter as a Node polyfill Buffer type once this
* is implemented. See https://github.com/denoland/deno/issues/3403
*/
export async function appendFile(
pathOrRid: string | number,
data: string,
optionsOrCallback: string | FileOptions | Function,
callback?: Function
): Promise<void> {
const callbackFn: Function | undefined =
optionsOrCallback instanceof Function ? optionsOrCallback : callback;
const options: string | FileOptions | undefined =
optionsOrCallback instanceof Function ? undefined : optionsOrCallback;
if (!callbackFn) {
throw new Error("No callback function supplied");
}

validateEncoding(options);

let rid = -1;
try {
if (typeof pathOrRid === "number") {
rid = pathOrRid;
} else {
const mode: number | undefined = isFileOptions(options)
? options.mode
: undefined;
const flag: string | undefined = isFileOptions(options)
? options.flag
: undefined;

if (mode) {
//TODO rework once https://github.com/denoland/deno/issues/4017 completes
notImplemented("Deno does not yet support setting mode on create");
}

const file = await Deno.open(pathOrRid, getOpenOptions(flag));
rid = file.rid;
}

const buffer: Uint8Array = new TextEncoder().encode(data);

await Deno.write(rid, buffer);
callbackFn();
} catch (err) {
callbackFn(err);
} finally {
if (typeof pathOrRid === "string" && rid != -1) {
//Only close if a path was supplied and a rid allocated
Deno.close(rid);
}
}
}

/**
* TODO: Also accept 'data' parameter as a Node polyfill Buffer type once this
* is implemented. See https://github.com/denoland/deno/issues/3403
*/
export function appendFileSync(
pathOrRid: string | number,
data: string,
options?: string | FileOptions
): void {
let rid = -1;

validateEncoding(options);

try {
if (typeof pathOrRid === "number") {
rid = pathOrRid;
} else {
const mode: number | undefined = isFileOptions(options)
? options.mode
: undefined;
const flag: string | undefined = isFileOptions(options)
? options.flag
: undefined;

if (mode) {
// TODO rework once https://github.com/denoland/deno/issues/4017 completes
notImplemented("Deno does not yet support setting mode on create");
}

const file = Deno.openSync(pathOrRid, getOpenOptions(flag));
rid = file.rid;
}

const buffer: Uint8Array = new TextEncoder().encode(data);

Deno.writeSync(rid, buffer);
} finally {
if (typeof pathOrRid === "string" && rid != -1) {
//Only close if a 'string' path was supplied and a rid allocated
Deno.close(rid);
}
}
}

function validateEncoding(
encodingOption: string | FileOptions | undefined
): void {
if (!encodingOption) return;

if (typeof encodingOption === "string") {
if (encodingOption !== "utf8") {
throw new Error("Only 'utf8' encoding is currently supported");
}
} else if (encodingOption.encoding && encodingOption.encoding !== "utf8") {
throw new Error("Only 'utf8' encoding is currently supported");
}
}

function getOpenOptions(flag: string | undefined): Deno.OpenOptions {
if (!flag) {
return { create: true, append: true };
}

let openOptions: Deno.OpenOptions;
switch (flag) {
case "a": {
// 'a': Open file for appending. The file is created if it does not exist.
openOptions = { create: true, append: true };
break;
}
case "ax": {
// 'ax': Like 'a' but fails if the path exists.
openOptions = { createNew: true, write: true, append: true };
break;
}
case "a+": {
// 'a+': Open file for reading and appending. The file is created if it does not exist.
openOptions = { read: true, create: true, append: true };
break;
}
case "ax+": {
// 'ax+': Like 'a+' but fails if the path exists.
openOptions = { read: true, createNew: true, append: true };
break;
}
case "r": {
// 'r': Open file for reading. An exception occurs if the file does not exist.
openOptions = { read: true };
break;
}
case "r+": {
// 'r+': Open file for reading and writing. An exception occurs if the file does not exist.
openOptions = { read: true, write: true };
break;
}
case "w": {
// 'w': Open file for writing. The file is created (if it does not exist) or truncated (if it exists).
openOptions = { create: true, write: true, truncate: true };
break;
}
case "wx": {
// 'wx': Like 'w' but fails if the path exists.
openOptions = { createNew: true, write: true };
break;
}
case "w+": {
// 'w+': Open file for reading and writing. The file is created (if it does not exist) or truncated (if it exists).
openOptions = { create: true, write: true, truncate: true, read: true };
break;
}
case "wx+": {
// 'wx+': Like 'w+' but fails if the path exists.
openOptions = { createNew: true, write: true, read: true };
break;
}
case "as": {
// 'as': Open file for appending in synchronous mode. The file is created if it does not exist.
openOptions = { create: true, append: true };
}
case "as+": {
// 'as+': Open file for reading and appending in synchronous mode. The file is created if it does not exist.
openOptions = { create: true, read: true, append: true };
}
case "rs+": {
// 'rs+': Open file for reading and writing in synchronous mode. Instructs the operating system to bypass the local file system cache.
openOptions = { create: true, read: true, write: true };
}
default: {
throw new Error(`Unrecognized file system flag: ${flag}`);
}
}

return openOptions;
}
166 changes: 166 additions & 0 deletions std/node/_fs/_fs_appendFile_test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license.
const { test } = Deno;
import {
assertEquals,
assert,
assertThrows,
assertThrowsAsync
} from "../../testing/asserts.ts";
import { appendFile, appendFileSync } from "./_fs_appendFile.ts";

const decoder = new TextDecoder("utf-8");

test({
name: "No callback Fn results in Error",
async fn() {
await assertThrowsAsync(
async () => {
await appendFile("some/path", "some data", "utf8");
},
Error,
"No callback function supplied"
);
}
});

test({
name: "Unsupported encoding results in error()",
async fn() {
await assertThrowsAsync(
async () => {
await appendFile(
"some/path",
"some data",
"made-up-encoding",
() => {}
);
},
Error,
"Only 'utf8' encoding is currently supported"
);
await assertThrowsAsync(
async () => {
await appendFile(
"some/path",
"some data",
{ encoding: "made-up-encoding" },
() => {}
);
},
Error,
"Only 'utf8' encoding is currently supported"
);
assertThrows(
() => appendFileSync("some/path", "some data", "made-up-encoding"),
Error,
"Only 'utf8' encoding is currently supported"
);
assertThrows(
() =>
appendFileSync("some/path", "some data", {
encoding: "made-up-encoding"
}),
Error,
"Only 'utf8' encoding is currently supported"
);
}
});

test({
name: "Async: Data is written to passed in rid",
async fn() {
const tempFile: string = await Deno.makeTempFile();
const file: Deno.File = await Deno.open(tempFile, {
create: true,
write: true,
read: true
});
let calledBack = false;
await appendFile(file.rid, "hello world", () => {
calledBack = true;
});
assert(calledBack);
Deno.close(file.rid);
const data = await Deno.readFile(tempFile);
assertEquals(decoder.decode(data), "hello world");
await Deno.remove(tempFile);
}
});

test({
name: "Async: Data is written to passed in file path",
async fn() {
let calledBack = false;
const openResourcesBeforeAppend: Deno.ResourceMap = Deno.resources();
await appendFile("_fs_appendFile_test_file.txt", "hello world", () => {
calledBack = true;
});
assert(calledBack);
assertEquals(Deno.resources(), openResourcesBeforeAppend);
const data = await Deno.readFile("_fs_appendFile_test_file.txt");
assertEquals(decoder.decode(data), "hello world");
await Deno.remove("_fs_appendFile_test_file.txt");
}
});

test({
name:
"Async: Callback is made with error if attempting to append data to an existing file with 'ax' flag",
async fn() {
let calledBack = false;
const openResourcesBeforeAppend: Deno.ResourceMap = Deno.resources();
const tempFile: string = await Deno.makeTempFile();
await appendFile(tempFile, "hello world", { flag: "ax" }, (err: Error) => {
calledBack = true;
assert(err);
});
assert(calledBack);
assertEquals(Deno.resources(), openResourcesBeforeAppend);
await Deno.remove(tempFile);
}
});

test({
name: "Sync: Data is written to passed in rid",
fn() {
const tempFile: string = Deno.makeTempFileSync();
const file: Deno.File = Deno.openSync(tempFile, {
create: true,
write: true,
read: true
});
appendFileSync(file.rid, "hello world");
Deno.close(file.rid);
const data = Deno.readFileSync(tempFile);
assertEquals(decoder.decode(data), "hello world");
Deno.removeSync(tempFile);
}
});

test({
name: "Sync: Data is written to passed in file path",
fn() {
const openResourcesBeforeAppend: Deno.ResourceMap = Deno.resources();
appendFileSync("_fs_appendFile_test_file_sync.txt", "hello world");
assertEquals(Deno.resources(), openResourcesBeforeAppend);
const data = Deno.readFileSync("_fs_appendFile_test_file_sync.txt");
assertEquals(decoder.decode(data), "hello world");
Deno.removeSync("_fs_appendFile_test_file_sync.txt");
}
});

test({
name:
"Sync: error thrown if attempting to append data to an existing file with 'ax' flag",
fn() {
const openResourcesBeforeAppend: Deno.ResourceMap = Deno.resources();
const tempFile: string = Deno.makeTempFileSync();
assertThrows(
() => appendFileSync(tempFile, "hello world", { flag: "ax" }),
Deno.errors.AlreadyExists,
""
);
assertEquals(Deno.resources(), openResourcesBeforeAppend);
Deno.removeSync(tempFile);
}
});
18 changes: 18 additions & 0 deletions std/node/_fs/_fs_common.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license.
export interface FileOptions {
encoding?: string;
mode?: number;
flag?: string;
}

export function isFileOptions(
fileOptions: string | FileOptions | undefined
): fileOptions is FileOptions {
if (!fileOptions) return false;

return (
(fileOptions as FileOptions).encoding != undefined ||
(fileOptions as FileOptions).flag != undefined ||
(fileOptions as FileOptions).mode != undefined
);
}
Loading