fix: Node polyfill fsAppend rework (#4322)

* My original implementation of `fs.appendFile` used an async API, which, though 
  it would work fine as a polyfill, wasn't an exact match with the Node API.  This PR
  reworks that API to mimic the Node API fully as a synchronous void function with
  an async internal implementation.
* Refactor move of other internal fs `dirent` and `dir` classes to the _fs internal
  directory.
This commit is contained in:
Chris Knight 2020-03-12 14:12:27 +00:00 committed by GitHub
parent 3ed6ccc905
commit cabe63eb05
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 111 additions and 87 deletions

View file

@ -1,18 +1,18 @@
// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. // Copyright 2018-2020 the Deno authors. All rights reserved. MIT license.
import { FileOptions, isFileOptions } from "./_fs_common.ts"; import { FileOptions, isFileOptions, CallbackWithError } from "./_fs_common.ts";
import { notImplemented } from "../_utils.ts"; import { notImplemented } from "../_utils.ts";
/** /**
* TODO: Also accept 'data' parameter as a Node polyfill Buffer type once this * TODO: Also accept 'data' parameter as a Node polyfill Buffer type once this
* is implemented. See https://github.com/denoland/deno/issues/3403 * is implemented. See https://github.com/denoland/deno/issues/3403
*/ */
export async function appendFile( export function appendFile(
pathOrRid: string | number, pathOrRid: string | number,
data: string, data: string,
optionsOrCallback: string | FileOptions | Function, optionsOrCallback: string | FileOptions | CallbackWithError,
callback?: Function callback?: CallbackWithError
): Promise<void> { ): void {
const callbackFn: Function | undefined = const callbackFn: CallbackWithError | undefined =
optionsOrCallback instanceof Function ? optionsOrCallback : callback; optionsOrCallback instanceof Function ? optionsOrCallback : callback;
const options: string | FileOptions | undefined = const options: string | FileOptions | undefined =
optionsOrCallback instanceof Function ? undefined : optionsOrCallback; optionsOrCallback instanceof Function ? undefined : optionsOrCallback;
@ -23,6 +23,7 @@ export async function appendFile(
validateEncoding(options); validateEncoding(options);
let rid = -1; let rid = -1;
new Promise(async (resolve, reject) => {
try { try {
if (typeof pathOrRid === "number") { if (typeof pathOrRid === "number") {
rid = pathOrRid; rid = pathOrRid;
@ -38,7 +39,6 @@ export async function appendFile(
//TODO rework once https://github.com/denoland/deno/issues/4017 completes //TODO rework once https://github.com/denoland/deno/issues/4017 completes
notImplemented("Deno does not yet support setting mode on create"); notImplemented("Deno does not yet support setting mode on create");
} }
const file = await Deno.open(pathOrRid, getOpenOptions(flag)); const file = await Deno.open(pathOrRid, getOpenOptions(flag));
rid = file.rid; rid = file.rid;
} }
@ -46,15 +46,26 @@ export async function appendFile(
const buffer: Uint8Array = new TextEncoder().encode(data); const buffer: Uint8Array = new TextEncoder().encode(data);
await Deno.write(rid, buffer); await Deno.write(rid, buffer);
callbackFn(); resolve();
} catch (err) { } catch (err) {
reject(err);
}
})
.then(() => {
closeRidIfNecessary(typeof pathOrRid === "string", rid);
callbackFn();
})
.catch(err => {
closeRidIfNecessary(typeof pathOrRid === "string", rid);
callbackFn(err); callbackFn(err);
} finally { });
if (typeof pathOrRid === "string" && rid != -1) { }
function closeRidIfNecessary(isPathString: boolean, rid: number): void {
if (isPathString && rid != -1) {
//Only close if a path was supplied and a rid allocated //Only close if a path was supplied and a rid allocated
Deno.close(rid); Deno.close(rid);
} }
}
} }
/** /**
@ -94,10 +105,7 @@ export function appendFileSync(
Deno.writeSync(rid, buffer); Deno.writeSync(rid, buffer);
} finally { } finally {
if (typeof pathOrRid === "string" && rid != -1) { closeRidIfNecessary(typeof pathOrRid === "string", rid);
//Only close if a 'string' path was supplied and a rid allocated
Deno.close(rid);
}
} }
} }

View file

@ -1,21 +1,16 @@
// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. // Copyright 2018-2020 the Deno authors. All rights reserved. MIT license.
const { test } = Deno; const { test } = Deno;
import { import { assertEquals, assertThrows, fail } from "../../testing/asserts.ts";
assertEquals,
assert,
assertThrows,
assertThrowsAsync
} from "../../testing/asserts.ts";
import { appendFile, appendFileSync } from "./_fs_appendFile.ts"; import { appendFile, appendFileSync } from "./_fs_appendFile.ts";
const decoder = new TextDecoder("utf-8"); const decoder = new TextDecoder("utf-8");
test({ test({
name: "No callback Fn results in Error", name: "No callback Fn results in Error",
async fn() { fn() {
await assertThrowsAsync( assertThrows(
async () => { () => {
await appendFile("some/path", "some data", "utf8"); appendFile("some/path", "some data", "utf8");
}, },
Error, Error,
"No callback function supplied" "No callback function supplied"
@ -25,22 +20,17 @@ test({
test({ test({
name: "Unsupported encoding results in error()", name: "Unsupported encoding results in error()",
async fn() { fn() {
await assertThrowsAsync( assertThrows(
async () => { () => {
await appendFile( appendFile("some/path", "some data", "made-up-encoding", () => {});
"some/path",
"some data",
"made-up-encoding",
() => {}
);
}, },
Error, Error,
"Only 'utf8' encoding is currently supported" "Only 'utf8' encoding is currently supported"
); );
await assertThrowsAsync( assertThrows(
async () => { () => {
await appendFile( appendFile(
"some/path", "some/path",
"some data", "some data",
{ encoding: "made-up-encoding" }, { encoding: "made-up-encoding" },
@ -75,31 +65,47 @@ test({
write: true, write: true,
read: true read: true
}); });
let calledBack = false; await new Promise((resolve, reject) => {
await appendFile(file.rid, "hello world", () => { appendFile(file.rid, "hello world", err => {
calledBack = true; if (err) reject();
else resolve();
}); });
assert(calledBack); })
Deno.close(file.rid); .then(async () => {
const data = await Deno.readFile(tempFile); const data = await Deno.readFile(tempFile);
assertEquals(decoder.decode(data), "hello world"); assertEquals(decoder.decode(data), "hello world");
})
.catch(() => {
fail("No error expected");
})
.finally(async () => {
Deno.close(file.rid);
await Deno.remove(tempFile); await Deno.remove(tempFile);
});
} }
}); });
test({ test({
name: "Async: Data is written to passed in file path", name: "Async: Data is written to passed in file path",
async fn() { async fn() {
let calledBack = false;
const openResourcesBeforeAppend: Deno.ResourceMap = Deno.resources(); const openResourcesBeforeAppend: Deno.ResourceMap = Deno.resources();
await appendFile("_fs_appendFile_test_file.txt", "hello world", () => { await new Promise((resolve, reject) => {
calledBack = true; appendFile("_fs_appendFile_test_file.txt", "hello world", err => {
if (err) reject(err);
else resolve();
}); });
assert(calledBack); })
.then(async () => {
assertEquals(Deno.resources(), openResourcesBeforeAppend); assertEquals(Deno.resources(), openResourcesBeforeAppend);
const data = await Deno.readFile("_fs_appendFile_test_file.txt"); const data = await Deno.readFile("_fs_appendFile_test_file.txt");
assertEquals(decoder.decode(data), "hello world"); assertEquals(decoder.decode(data), "hello world");
})
.catch(err => {
fail("No error was expected: " + err);
})
.finally(async () => {
await Deno.remove("_fs_appendFile_test_file.txt"); await Deno.remove("_fs_appendFile_test_file.txt");
});
} }
}); });
@ -107,16 +113,23 @@ test({
name: name:
"Async: Callback is made with error if attempting to append data to an existing file with 'ax' flag", "Async: Callback is made with error if attempting to append data to an existing file with 'ax' flag",
async fn() { async fn() {
let calledBack = false;
const openResourcesBeforeAppend: Deno.ResourceMap = Deno.resources(); const openResourcesBeforeAppend: Deno.ResourceMap = Deno.resources();
const tempFile: string = await Deno.makeTempFile(); const tempFile: string = await Deno.makeTempFile();
await appendFile(tempFile, "hello world", { flag: "ax" }, (err: Error) => { await new Promise((resolve, reject) => {
calledBack = true; appendFile(tempFile, "hello world", { flag: "ax" }, err => {
assert(err); if (err) reject(err);
else resolve();
}); });
assert(calledBack); })
.then(() => {
fail("Expected error to be thrown");
})
.catch(() => {
assertEquals(Deno.resources(), openResourcesBeforeAppend); assertEquals(Deno.resources(), openResourcesBeforeAppend);
})
.finally(async () => {
await Deno.remove(tempFile); await Deno.remove(tempFile);
});
} }
}); });

View file

@ -1,4 +1,7 @@
// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. // Copyright 2018-2020 the Deno authors. All rights reserved. MIT license.
export type CallbackWithError = (err?: Error) => void;
export interface FileOptions { export interface FileOptions {
encoding?: string; encoding?: string;
mode?: number; mode?: number;

View file

@ -1,5 +1,5 @@
const { test } = Deno; const { test } = Deno;
import { assert, assertEquals, fail } from "../testing/asserts.ts"; import { assert, assertEquals, fail } from "../../testing/asserts.ts";
import Dir from "./_fs_dir.ts"; import Dir from "./_fs_dir.ts";
import Dirent from "./_fs_dirent.ts"; import Dirent from "./_fs_dirent.ts";

View file

@ -1,4 +1,4 @@
import { notImplemented } from "./_utils.ts"; import { notImplemented } from "../_utils.ts";
export default class Dirent { export default class Dirent {
constructor(private entry: Deno.FileInfo) {} constructor(private entry: Deno.FileInfo) {}

View file

@ -1,5 +1,5 @@
const { test } = Deno; const { test } = Deno;
import { assert, assertEquals, assertThrows } from "../testing/asserts.ts"; import { assert, assertEquals, assertThrows } from "../../testing/asserts.ts";
import Dirent from "./_fs_dirent.ts"; import Dirent from "./_fs_dirent.ts";
class FileInfoMock implements Deno.FileInfo { class FileInfoMock implements Deno.FileInfo {