diff --git a/BUILD.gn b/BUILD.gn index 285d6ee13b..5d53ae41ff 100644 --- a/BUILD.gn +++ b/BUILD.gn @@ -101,6 +101,7 @@ ts_sources = [ "js/util.ts", "js/v8_source_maps.ts", "js/write_file.ts", + "js/copy_file.ts", "tsconfig.json", # Listing package.json and yarn.lock as sources ensures the bundle is rebuilt diff --git a/js/copy_file.ts b/js/copy_file.ts new file mode 100644 index 0000000000..ff08cc00b1 --- /dev/null +++ b/js/copy_file.ts @@ -0,0 +1,46 @@ +// Copyright 2018 the Deno authors. All rights reserved. MIT license. +import * as fbs from "gen/msg_generated"; +import { flatbuffers } from "flatbuffers"; +import * as dispatch from "./dispatch"; + +/** + * Copies the contents of a file to another by name synchronously. + * Creates a new file if target does not exists, and if target exists, + * overwrites original content of the target file. + * It would also copy the permission of the original file + * to the destination. + * + * import { copyFileSync } from "deno"; + * copyFileSync("from.txt", "to.txt"); + */ +export function copyFileSync(from: string, to: string): void { + dispatch.sendSync(...req(from, to)); +} + +/** + * Copies the contents of a file to another by name. + * Creates a new file if target does not exists, and if target exists, + * overwrites original content of the target file. + * It would also copy the permission of the original file + * to the destination. + * + * import { copyFile } from "deno"; + * await copyFile("from.txt", "to.txt"); + */ +export async function copyFile(from: string, to: string): Promise { + await dispatch.sendAsync(...req(from, to)); +} + +function req( + from: string, + to: string +): [flatbuffers.Builder, fbs.Any, flatbuffers.Offset] { + const builder = new flatbuffers.Builder(); + const from_ = builder.createString(from); + const to_ = builder.createString(to); + fbs.CopyFile.startCopyFile(builder); + fbs.CopyFile.addFrom(builder, from_); + fbs.CopyFile.addTo(builder, to_); + const msg = fbs.CopyFile.endCopyFile(builder); + return [builder, fbs.Any.CopyFile, msg]; +} diff --git a/js/copy_file_test.ts b/js/copy_file_test.ts new file mode 100644 index 0000000000..846cfcbc42 --- /dev/null +++ b/js/copy_file_test.ts @@ -0,0 +1,130 @@ +import { testPerm, assert, assertEqual } from "./test_util.ts"; +import * as deno from "deno"; + +function readFileString(filename: string): string { + const dataRead = deno.readFileSync(filename); + const dec = new TextDecoder("utf-8"); + return dec.decode(dataRead); +} + +function writeFileString(filename: string, s: string) { + const enc = new TextEncoder(); + const data = enc.encode(s); + deno.writeFileSync(filename, data, 0o666); +} + +function assertSameContent(filename1: string, filename2: string) { + const data1 = deno.readFileSync(filename1); + const data2 = deno.readFileSync(filename2); + assertEqual(data1, data2); +} + +testPerm({ write: true }, function copyFileSyncSuccess() { + const tempDir = deno.makeTempDirSync(); + const fromFilename = tempDir + "/from.txt"; + const toFilename = tempDir + "/to.txt"; + writeFileString(fromFilename, "Hello world!"); + deno.copyFileSync(fromFilename, toFilename); + // No change to original file + assertEqual(readFileString(fromFilename), "Hello world!"); + // Original == Dest + assertSameContent(fromFilename, toFilename); +}); + +testPerm({ write: true }, function copyFileSyncFailure() { + const tempDir = deno.makeTempDirSync(); + const fromFilename = tempDir + "/from.txt"; + const toFilename = tempDir + "/to.txt"; + // We skip initial writing here, from.txt does not exist + let err; + try { + deno.copyFileSync(fromFilename, toFilename); + } catch (e) { + err = e; + } + assert(!!err); + // Rust deem non-existent path as invalid input + assertEqual(err.kind, deno.ErrorKind.InvalidInput); + assertEqual(err.name, "InvalidInput"); +}); + +testPerm({ write: true }, function copyFileSyncOverwrite() { + const tempDir = deno.makeTempDirSync(); + const fromFilename = tempDir + "/from.txt"; + const toFilename = tempDir + "/to.txt"; + writeFileString(fromFilename, "Hello world!"); + // Make Dest exist and have different content + writeFileString(toFilename, "Goodbye!"); + deno.copyFileSync(fromFilename, toFilename); + // No change to original file + assertEqual(readFileString(fromFilename), "Hello world!"); + // Original == Dest + assertSameContent(fromFilename, toFilename); +}); + +testPerm({ write: false }, function copyFileSyncPerm() { + let err; + try { + deno.copyFileSync("/from.txt", "/to.txt"); + } catch (e) { + err = e; + } + assert(!!err); + assertEqual(err.kind, deno.ErrorKind.PermissionDenied); + assertEqual(err.name, "PermissionDenied"); +}); + +testPerm({ write: true }, async function copyFileSuccess() { + const tempDir = deno.makeTempDirSync(); + const fromFilename = tempDir + "/from.txt"; + const toFilename = tempDir + "/to.txt"; + writeFileString(fromFilename, "Hello world!"); + await deno.copyFile(fromFilename, toFilename); + // No change to original file + assertEqual(readFileString(fromFilename), "Hello world!"); + // Original == Dest + assertSameContent(fromFilename, toFilename); +}); + +testPerm({ write: true }, async function copyFileFailure() { + const tempDir = deno.makeTempDirSync(); + const fromFilename = tempDir + "/from.txt"; + const toFilename = tempDir + "/to.txt"; + // We skip initial writing here, from.txt does not exist + let err; + try { + await deno.copyFile(fromFilename, toFilename); + } catch (e) { + err = e; + } + assert(!!err); + // Rust deem non-existent path as invalid input + assertEqual(err.kind, deno.ErrorKind.InvalidInput); + assertEqual(err.name, "InvalidInput"); +}); + +testPerm({ write: true }, async function copyFileOverwrite() { + const tempDir = deno.makeTempDirSync(); + const fromFilename = tempDir + "/from.txt"; + const toFilename = tempDir + "/to.txt"; + writeFileString(fromFilename, "Hello world!"); + // Make Dest exist and have different content + writeFileString(toFilename, "Goodbye!"); + await deno.copyFile(fromFilename, toFilename); + // No change to original file + assertEqual(readFileString(fromFilename), "Hello world!"); + // Original == Dest + assertSameContent(fromFilename, toFilename); +}); + +testPerm({ write: false }, async function copyFilePerm() { + let err; + try { + await deno.copyFile("/from.txt", "/to.txt"); + } catch (e) { + err = e; + } + assert(!!err); + assertEqual(err.kind, deno.ErrorKind.PermissionDenied); + assertEqual(err.name, "PermissionDenied"); +}); diff --git a/js/deno.ts b/js/deno.ts index cd290bfa38..03f3d1a89d 100644 --- a/js/deno.ts +++ b/js/deno.ts @@ -9,6 +9,7 @@ export { makeTempDirSync, makeTempDir } from "./make_temp_dir"; export { removeSync, remove, removeAllSync, removeAll } from "./remove"; export { renameSync, rename } from "./rename"; export { readFileSync, readFile } from "./read_file"; +export { copyFileSync, copyFile } from "./copy_file"; export { readlinkSync, readlink } from "./read_link"; export { FileInfo, statSync, lstatSync, stat, lstat } from "./stat"; export { symlinkSync, symlink } from "./symlink"; diff --git a/js/unit_tests.ts b/js/unit_tests.ts index 2eea6c17b8..e33fcf245b 100644 --- a/js/unit_tests.ts +++ b/js/unit_tests.ts @@ -8,6 +8,7 @@ import "./os_test.ts"; import "./files_test.ts"; import "./read_file_test.ts"; import "./write_file_test.ts"; +import "./copy_file_test.ts"; import "./mkdir_test.ts"; import "./make_temp_dir_test.ts"; import "./stat_test.ts"; diff --git a/src/handlers.rs b/src/handlers.rs index c615f56693..1f861f0007 100644 --- a/src/handlers.rs +++ b/src/handlers.rs @@ -80,6 +80,7 @@ pub fn msg_from_js( msg::Any::Truncate => handle_truncate, msg::Any::WriteFile => handle_write_file, msg::Any::Exit => handle_exit, + msg::Any::CopyFile => handle_copy_file, _ => panic!(format!( "Unhandled message {}", msg::enum_name_any(msg_type) @@ -747,6 +748,27 @@ fn handle_read_file( }) } +fn handle_copy_file( + state: Arc, + base: &msg::Base, + data: &'static mut [u8], +) -> Box { + assert_eq!(data.len(), 0); + let msg = base.msg_as_copy_file().unwrap(); + let from = PathBuf::from(msg.from().unwrap()); + let to = PathBuf::from(msg.to().unwrap()); + + if !state.flags.allow_write { + return odd_future(permission_denied()); + } + + debug!("handle_copy_file {} {}", from.display(), to.display()); + blocking!(base.sync(), || { + fs::copy(&from, &to)?; + Ok(empty_buf()) + }) +} + macro_rules! to_seconds { ($time:expr) => {{ // Unwrap is safe here as if the file is before the unix epoch diff --git a/src/msg.fbs b/src/msg.fbs index 8b95ec7416..ef2f4946bb 100644 --- a/src/msg.fbs +++ b/src/msg.fbs @@ -20,6 +20,7 @@ union Any { ReadFile, ReadFileRes, WriteFile, + CopyFile, Rename, Readlink, ReadlinkRes, @@ -214,6 +215,11 @@ table WriteFile { // perm specified by https://godoc.org/os#FileMode } +table CopyFile { + from: string; + to: string; +} + table Rename { oldpath: string; newpath: string;