0
0
Fork 0
mirror of https://github.com/denoland/deno.git synced 2025-03-03 09:31:22 -05:00

feat(std/node): fs.writefile / fs.promises.writeFile (#5054)

This commit is contained in:
Marcos Casagrande 2020-05-05 00:59:37 +02:00 committed by GitHub
parent 5f67a202ff
commit f0aea98c85
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 522 additions and 121 deletions

View file

@ -1,5 +1,10 @@
// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license.
import { FileOptions, isFileOptions, CallbackWithError } from "./_fs_common.ts";
import {
WriteFileOptions,
isFileOptions,
CallbackWithError,
getOpenOptions,
} from "./_fs_common.ts";
import { notImplemented } from "../_utils.ts";
import { fromFileUrl } from "../path.ts";
@ -10,13 +15,13 @@ import { fromFileUrl } from "../path.ts";
export function appendFile(
pathOrRid: string | number | URL,
data: string,
optionsOrCallback: string | FileOptions | CallbackWithError,
optionsOrCallback: string | WriteFileOptions | CallbackWithError,
callback?: CallbackWithError
): void {
pathOrRid = pathOrRid instanceof URL ? fromFileUrl(pathOrRid) : pathOrRid;
const callbackFn: CallbackWithError | undefined =
optionsOrCallback instanceof Function ? optionsOrCallback : callback;
const options: string | FileOptions | undefined =
const options: string | WriteFileOptions | undefined =
optionsOrCallback instanceof Function ? undefined : optionsOrCallback;
if (!callbackFn) {
throw new Error("No callback function supplied");
@ -74,7 +79,7 @@ function closeRidIfNecessary(isPathString: boolean, rid: number): void {
export function appendFileSync(
pathOrRid: string | number | URL,
data: string,
options?: string | FileOptions
options?: string | WriteFileOptions
): void {
let rid = -1;
@ -110,7 +115,7 @@ export function appendFileSync(
}
function validateEncoding(
encodingOption: string | FileOptions | undefined
encodingOption: string | WriteFileOptions | undefined
): void {
if (!encodingOption) return;
@ -122,80 +127,3 @@ function validateEncoding(
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;
}

View file

@ -1,21 +1,137 @@
// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license.
export type CallbackWithError = (err?: Error) => void;
import { notImplemented } from "../_utils.ts";
export type CallbackWithError = (err?: Error | null) => void;
export interface FileOptions {
encoding?: string;
mode?: number;
flag?: string;
}
export interface WriteFileOptions extends FileOptions {
mode?: number;
}
export function isFileOptions(
fileOptions: string | FileOptions | undefined
fileOptions: string | WriteFileOptions | undefined
): fileOptions is FileOptions {
if (!fileOptions) return false;
return (
(fileOptions as FileOptions).encoding != undefined ||
(fileOptions as FileOptions).flag != undefined ||
(fileOptions as FileOptions).mode != undefined
(fileOptions as WriteFileOptions).mode != undefined
);
}
export function getEncoding(
optOrCallback?: FileOptions | WriteFileOptions | Function | string
): string | null {
if (!optOrCallback || typeof optOrCallback === "function") {
return null;
}
const encoding =
typeof optOrCallback === "string" ? optOrCallback : optOrCallback.encoding;
if (!encoding) return null;
if (encoding === "utf8" || encoding === "utf-8") {
return "utf8";
}
if (encoding === "buffer") {
return "buffer";
}
const notImplementedEncodings = [
"utf16le",
"latin1",
"base64",
"hex",
"ascii",
"binary",
"ucs2",
];
if (notImplementedEncodings.includes(encoding)) {
notImplemented(`"${encoding}" encoding`);
}
throw new Error(`The value "${encoding}" is invalid for option "encoding"`);
}
export 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;
}

View file

@ -12,7 +12,7 @@ test({
fn: async () => {
const srouceFile = Deno.makeTempFileSync();
const err = await new Promise((resolve) => {
copyFile(srouceFile, destFile, (err: Error | undefined) => resolve(err));
copyFile(srouceFile, destFile, (err?: Error | null) => resolve(err));
});
assert(!err);
assert(existsSync(destFile));

View file

@ -1,10 +1,8 @@
// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license.
import {
notImplemented,
intoCallbackAPIWithIntercept,
MaybeEmpty,
} from "../_utils.ts";
import { intoCallbackAPIWithIntercept, MaybeEmpty } from "../_utils.ts";
import { getEncoding, FileOptions } from "./_fs_common.ts";
import { fromFileUrl } from "../path.ts";
const { readFile: denoReadFile, readFileSync: denoReadFileSync } = Deno;
@ -14,33 +12,6 @@ type ReadFileCallback = (
data: MaybeEmpty<string | Uint8Array>
) => void;
interface ReadFileOptions {
encoding?: string | null;
flag?: string;
}
function getEncoding(
optOrCallback?: ReadFileOptions | ReadFileCallback
): string | null {
if (!optOrCallback || typeof optOrCallback === "function") {
return null;
} else {
if (optOrCallback.encoding) {
if (
optOrCallback.encoding === "utf8" ||
optOrCallback.encoding === "utf-8"
) {
return "utf8";
} else if (optOrCallback.encoding === "buffer") {
return "buffer";
} else {
notImplemented();
}
}
return null;
}
}
function maybeDecode(
data: Uint8Array,
encoding: string | null
@ -53,7 +24,7 @@ function maybeDecode(
export function readFile(
path: string | URL,
optOrCallback: ReadFileCallback | ReadFileOptions,
optOrCallback: ReadFileCallback | FileOptions,
callback?: ReadFileCallback
): void {
path = path instanceof URL ? fromFileUrl(path) : path;
@ -76,7 +47,7 @@ export function readFile(
export function readFileSync(
path: string | URL,
opt?: ReadFileOptions
opt?: FileOptions
): string | Uint8Array {
path = path instanceof URL ? fromFileUrl(path) : path;
return maybeDecode(denoReadFileSync(path), getEncoding(opt));

View file

@ -0,0 +1,65 @@
// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license.
import { notImplemented } from "../_utils.ts";
import {
WriteFileOptions,
CallbackWithError,
isFileOptions,
getEncoding,
getOpenOptions,
} from "./_fs_common.ts";
export function writeFile(
pathOrRid: string | number,
data: string | Uint8Array,
optOrCallback: string | CallbackWithError | WriteFileOptions | undefined,
callback?: CallbackWithError
): void {
const callbackFn: CallbackWithError | undefined =
optOrCallback instanceof Function ? optOrCallback : callback;
const options: string | WriteFileOptions | undefined =
optOrCallback instanceof Function ? undefined : optOrCallback;
if (!callbackFn) {
throw new TypeError("Callback must be a function.");
}
const flag: string | undefined = isFileOptions(options)
? options.flag
: undefined;
const mode: number | undefined = isFileOptions(options)
? options.mode
: undefined;
const encoding = getEncoding(options) || "utf8";
const openOptions = getOpenOptions(flag || "w");
if (typeof data === "string" && encoding === "utf8")
data = new TextEncoder().encode(data) as Uint8Array;
const isRid = typeof pathOrRid === "number";
let file;
let error: Error | null = null;
(async (): Promise<void> => {
try {
file = isRid
? new Deno.File(pathOrRid as number)
: await Deno.open(pathOrRid as string, openOptions);
if (!isRid && mode) {
if (Deno.build.os === "windows") notImplemented(`"mode" on Windows`);
await Deno.chmod(pathOrRid as string, mode);
}
await Deno.writeAll(file, data as Uint8Array);
} catch (e) {
error = e;
} finally {
// Make sure to close resource
if (!isRid && file) file.close();
callbackFn(error);
}
})();
}

View file

@ -0,0 +1,180 @@
// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license.
const { test } = Deno;
import {
assert,
assertEquals,
assertNotEquals,
assertThrows,
assertThrowsAsync,
} from "../../testing/asserts.ts";
import { writeFile } from "./_fs_writeFile.ts";
const decoder = new TextDecoder("utf-8");
test("Invalid encoding results in error()", function fn() {
assertThrows(
() => {
writeFile("some/path", "some data", "utf8");
},
TypeError,
"Callback must be a function."
);
});
test("Invalid encoding results in error()", function testEncodingErrors() {
assertThrows(
() => {
writeFile("some/path", "some data", "made-up-encoding", () => {});
},
Error,
`The value "made-up-encoding" is invalid for option "encoding"`
);
assertThrows(
() => {
writeFile(
"some/path",
"some data",
{
encoding: "made-up-encoding",
},
() => {}
);
},
Error,
`The value "made-up-encoding" is invalid for option "encoding"`
);
});
test("Unsupported encoding results in error()", function testUnsupportedEncoding() {
assertThrows(
() => {
writeFile("some/path", "some data", "hex", () => {});
},
Error,
`Not implemented: "hex" encoding`
);
assertThrows(
() => {
writeFile(
"some/path",
"some data",
{
encoding: "base64",
},
() => {}
);
},
Error,
`Not implemented: "base64" encoding`
);
});
test("Data is written to correct rid", async function testCorrectWriteUsingRid() {
const tempFile: string = await Deno.makeTempFile();
const file: Deno.File = await Deno.open(tempFile, {
create: true,
write: true,
read: true,
});
await new Promise((resolve, reject) => {
writeFile(file.rid, "hello world", (err) => {
if (err) return reject(err);
resolve();
});
});
Deno.close(file.rid);
const data = await Deno.readFile(tempFile);
await Deno.remove(tempFile);
assertEquals(decoder.decode(data), "hello world");
});
test("Data is written to correct rid", async function testCorrectWriteUsingRid() {
const tempFile: string = await Deno.makeTempFile();
const file: Deno.File = await Deno.open(tempFile, {
create: true,
write: true,
read: true,
});
await new Promise((resolve, reject) => {
writeFile(file.rid, "hello world", (err) => {
if (err) return reject(err);
resolve();
});
});
Deno.close(file.rid);
const data = await Deno.readFile(tempFile);
await Deno.remove(tempFile);
assertEquals(decoder.decode(data), "hello world");
});
test("Data is written to correct file", async function testCorrectWriteUsingPath() {
const res = await new Promise((resolve) => {
writeFile("_fs_writeFile_test_file.txt", "hello world", resolve);
});
const data = await Deno.readFile("_fs_writeFile_test_file.txt");
await Deno.remove("_fs_writeFile_test_file.txt");
assertEquals(res, null);
assertEquals(decoder.decode(data), "hello world");
});
test("Mode is correctly set", async function testCorrectFileMode() {
if (Deno.build.os === "windows") return;
const filename = "_fs_writeFile_test_file.txt";
const res = await new Promise((resolve) => {
writeFile(filename, "hello world", { mode: 0o777 }, resolve);
});
const fileInfo = await Deno.stat(filename);
await Deno.remove(filename);
assertEquals(res, null);
assert(fileInfo && fileInfo.mode);
assertEquals(fileInfo.mode & 0o777, 0o777);
});
test("Mode is not set when rid is passed", async function testCorrectFileModeRid() {
if (Deno.build.os === "windows") return;
const filename: string = await Deno.makeTempFile();
const file: Deno.File = await Deno.open(filename, {
create: true,
write: true,
read: true,
});
await new Promise((resolve, reject) => {
writeFile(file.rid, "hello world", { mode: 0o777 }, (err) => {
if (err) return reject(err);
resolve();
});
});
Deno.close(file.rid);
const fileInfo = await Deno.stat(filename);
await Deno.remove(filename);
assert(fileInfo.mode);
assertNotEquals(fileInfo.mode & 0o777, 0o777);
});
test("Mode is not implemented on windows", function testModeNotImplementedWindows(): void {
if (Deno.build.os !== "windows") return;
assertThrowsAsync(
() => {
return new Promise((resolve, reject) => {
writeFile("fail.txt", "some data", { mode: 0o777 }, (err) => {
if (err) return reject(err);
resolve();
});
});
},
Error,
`Not implemented: "mode" on Windows`
);
});

View file

@ -0,0 +1,17 @@
// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license.
import { WriteFileOptions } from "../_fs_common.ts";
import { writeFile as writeFileCallback } from "../_fs_writeFile.ts";
export function writeFile(
pathOrRid: string | number,
data: string | Uint8Array,
options?: string | WriteFileOptions
): Promise<void> {
return new Promise((resolve, reject) => {
writeFileCallback(pathOrRid, data, options, (err?: Error | null) => {
if (err) return reject(err);
resolve();
});
});
}

View file

@ -0,0 +1,119 @@
// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license.
const { test } = Deno;
import {
assert,
assertEquals,
assertNotEquals,
assertThrowsAsync,
} from "../../../testing/asserts.ts";
import { writeFile } from "./_fs_writeFile.ts";
const decoder = new TextDecoder("utf-8");
test("Invalid encoding results in error()", function testEncodingErrors() {
assertThrowsAsync(
async () => {
await writeFile("some/path", "some data", "made-up-encoding");
},
Error,
`The value "made-up-encoding" is invalid for option "encoding"`
);
assertThrowsAsync(
async () => {
await writeFile("some/path", "some data", {
encoding: "made-up-encoding",
});
},
Error,
`The value "made-up-encoding" is invalid for option "encoding"`
);
});
test("Unsupported encoding results in error()", function testUnsupportedEncoding() {
assertThrowsAsync(
async () => {
await writeFile("some/path", "some data", "hex");
},
Error,
`Not implemented: "hex" encoding`
);
assertThrowsAsync(
async () => {
await writeFile("some/path", "some data", {
encoding: "base64",
});
},
Error,
`Not implemented: "base64" encoding`
);
});
test("Data is written to correct rid", async function testCorrectWriteUsingRid() {
const tempFile: string = await Deno.makeTempFile();
const file: Deno.File = await Deno.open(tempFile, {
create: true,
write: true,
read: true,
});
await writeFile(file.rid, "hello world");
Deno.close(file.rid);
const data = await Deno.readFile(tempFile);
await Deno.remove(tempFile);
assertEquals(decoder.decode(data), "hello world");
});
test("Data is written to correct file", async function testCorrectWriteUsingPath() {
const openResourcesBeforeWrite: Deno.ResourceMap = Deno.resources();
await writeFile("_fs_writeFile_test_file.txt", "hello world");
assertEquals(Deno.resources(), openResourcesBeforeWrite);
const data = await Deno.readFile("_fs_writeFile_test_file.txt");
await Deno.remove("_fs_writeFile_test_file.txt");
assertEquals(decoder.decode(data), "hello world");
});
test("Mode is correctly set", async function testCorrectFileMode() {
if (Deno.build.os === "windows") return;
const filename = "_fs_writeFile_test_file.txt";
await writeFile(filename, "hello world", { mode: 0o777 });
const fileInfo = await Deno.stat(filename);
await Deno.remove(filename);
assert(fileInfo && fileInfo.mode);
assertEquals(fileInfo.mode & 0o777, 0o777);
});
test("Mode is not set when rid is passed", async function testCorrectFileModeRid() {
if (Deno.build.os === "windows") return;
const filename: string = await Deno.makeTempFile();
const file: Deno.File = await Deno.open(filename, {
create: true,
write: true,
read: true,
});
await writeFile(file.rid, "hello world", { mode: 0o777 });
Deno.close(file.rid);
const fileInfo = await Deno.stat(filename);
await Deno.remove(filename);
assert(fileInfo.mode);
assertNotEquals(fileInfo.mode & 0o777, 0o777);
});
test("Mode is not implemented on windows", function testModeNotImplementedWindows(): void {
if (Deno.build.os !== "windows") return;
assertThrowsAsync(
async () => {
await writeFile("fail.txt", "some data", { mode: 0o777 });
},
Error,
`Not implemented: "mode" on Windows`
);
});

View file

@ -0,0 +1 @@
export { writeFile } from "./_fs_writeFile.ts";

View file

@ -11,6 +11,8 @@ import { readlink, readlinkSync } from "./_fs/_fs_readlink.ts";
import { exists, existsSync } from "./_fs/_fs_exists.ts";
import { mkdir, mkdirSync } from "./_fs/_fs_mkdir.ts";
import { copyFile, copyFileSync } from "./_fs/_fs_copy.ts";
import { writeFile } from "./_fs/_fs_writeFile.ts";
import * as promises from "./_fs/promises/mod.ts";
export {
access,
@ -34,4 +36,6 @@ export {
readlinkSync,
mkdir,
mkdirSync,
writeFile,
promises,
};