diff --git a/BUILD.gn b/BUILD.gn index 78d3507a74..839e904bec 100644 --- a/BUILD.gn +++ b/BUILD.gn @@ -47,6 +47,7 @@ main_extern = [ "$rust_build:rand", "$rust_build:tokio", "$rust_build:url", + "$rust_build:remove_dir_all", "//build_extra/flatbuffers/rust:flatbuffers", ":msg_rs", ] @@ -198,6 +199,7 @@ run_node("gen_declarations") { "js/mkdir.ts", "js/os.ts", "js/read_file.ts", + "js/remove.ts", "js/stat.ts", "js/text_encoding.ts", "js/timers.ts", @@ -205,6 +207,7 @@ run_node("gen_declarations") { "js/types.ts", "js/util.ts", "js/v8_source_maps.ts", + "js/write_file.ts", ] outputs = [ "$out_dir/types/globals.d.ts", @@ -239,12 +242,14 @@ run_node("bundle") { "js/os.ts", "js/plugins.d.ts", "js/read_file.ts", + "js/remove.ts", "js/stat.ts", "js/text_encoding.ts", "js/timers.ts", "js/types.ts", "js/util.ts", "js/v8_source_maps.ts", + "js/write_file.ts", "rollup.config.js", "src/msg.fbs", "tsconfig.json", diff --git a/js/deno.ts b/js/deno.ts index a1eb639bb4..3d0352a809 100644 --- a/js/deno.ts +++ b/js/deno.ts @@ -1,13 +1,9 @@ // Copyright 2018 the Deno authors. All rights reserved. MIT license. // Public deno module. /// -export { - env, - exit, - makeTempDirSync, - renameSync, -} from "./os"; +export { env, exit, makeTempDirSync, renameSync } from "./os"; export { mkdirSync, mkdir } from "./mkdir"; +export { removeSync, remove, removeAllSync, removeAll } from "./remove"; export { readFileSync, readFile } from "./read_file"; export { FileInfo, statSync, lstatSync, stat, lstat } from "./stat"; export { writeFileSync, writeFile } from "./write_file"; diff --git a/js/remove.ts b/js/remove.ts new file mode 100644 index 0000000000..9211107895 --- /dev/null +++ b/js/remove.ts @@ -0,0 +1,63 @@ +// 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"; + +/** + * Removes the named file or (empty) directory synchronously. + * Would throw error if permission denied, not found, or + * directory not empty. + * + * import { removeSync } from "deno"; + * removeSync("/path/to/empty_dir/or/file"); + */ +export function removeSync(path: string): void { + dispatch.sendSync(...req(path, false)); +} + +/** + * Removes the named file or (empty) directory. + * Would throw error if permission denied, not found, or + * directory not empty. + * + * import { remove } from "deno"; + * await remove("/path/to/empty_dir/or/file"); + */ +export async function remove(path: string): Promise { + await dispatch.sendAsync(...req(path, false)); +} + +/** + * Recursively removes the named file or directory synchronously. + * Would throw error if permission denied or not found + * + * import { removeAllSync } from "deno"; + * removeAllSync("/path/to/dir/or/file"); + */ +export function removeAllSync(path: string): void { + dispatch.sendSync(...req(path, true)); +} + +/** + * Recursively removes the named file or directory. + * Would throw error if permission denied or not found + * + * import { removeAll } from "deno"; + * await removeAll("/path/to/dir/or/file"); + */ +export async function removeAll(path: string): Promise { + await dispatch.sendAsync(...req(path, true)); +} + +function req( + path: string, + recursive: boolean +): [flatbuffers.Builder, fbs.Any, flatbuffers.Offset] { + const builder = new flatbuffers.Builder(); + const path_ = builder.createString(path); + fbs.Remove.startRemove(builder); + fbs.Remove.addPath(builder, path_); + fbs.Remove.addRecursive(builder, recursive); + const msg = fbs.Remove.endRemove(builder); + return [builder, fbs.Any.Remove, msg]; +} diff --git a/js/remove_test.ts b/js/remove_test.ts new file mode 100644 index 0000000000..eb31f1ba3e --- /dev/null +++ b/js/remove_test.ts @@ -0,0 +1,336 @@ +// Copyright 2018 the Deno authors. All rights reserved. MIT license. +import { testPerm, assert, assertEqual } from "./test_util.ts"; +import * as deno from "deno"; + +// SYNC + +testPerm({ write: true }, function removeSyncDirSuccess() { + // REMOVE EMPTY DIRECTORY + const path = deno.makeTempDirSync() + "/dir/subdir"; + deno.mkdirSync(path); + const pathInfo = deno.statSync(path); + assert(pathInfo.isDirectory()); // check exist first + deno.removeSync(path); // remove + // We then check again after remove + let err; + try { + deno.statSync(path); + } catch (e) { + err = e; + } + // Directory is gone + assertEqual(err.kind, deno.ErrorKind.NotFound); + assertEqual(err.name, "NotFound"); +}); + +testPerm({ write: true }, function removeSyncFileSuccess() { + // REMOVE FILE + const enc = new TextEncoder(); + const data = enc.encode("Hello"); + const filename = deno.makeTempDirSync() + "/test.txt"; + deno.writeFileSync(filename, data, 0o666); + const fileInfo = deno.statSync(filename); + assert(fileInfo.isFile()); // check exist first + deno.removeSync(filename); // remove + // We then check again after remove + let err; + try { + deno.statSync(filename); + } catch (e) { + err = e; + } + // File is gone + assertEqual(err.kind, deno.ErrorKind.NotFound); + assertEqual(err.name, "NotFound"); +}); + +testPerm({ write: true }, function removeSyncFail() { + // NON-EMPTY DIRECTORY + const path = deno.makeTempDirSync() + "/dir/subdir"; + const subPath = path + "/subsubdir"; + deno.mkdirSync(path); + deno.mkdirSync(subPath); + const pathInfo = deno.statSync(path); + assert(pathInfo.isDirectory()); // check exist first + const subPathInfo = deno.statSync(subPath); + assert(subPathInfo.isDirectory()); // check exist first + let err; + try { + // Should not be able to recursively remove + deno.removeSync(path); + } catch (e) { + err = e; + } + // TODO(ry) Is Other really the error we should get here? What would Go do? + assertEqual(err.kind, deno.ErrorKind.Other); + assertEqual(err.name, "Other"); + // NON-EXISTENT DIRECTORY/FILE + try { + // Non-existent + deno.removeSync("/baddir"); + } catch (e) { + err = e; + } + assertEqual(err.kind, deno.ErrorKind.NotFound); + assertEqual(err.name, "NotFound"); +}); + +testPerm({ write: false }, function removeSyncPerm() { + let err; + try { + deno.removeSync("/baddir"); + } catch (e) { + err = e; + } + assertEqual(err.kind, deno.ErrorKind.PermissionDenied); + assertEqual(err.name, "PermissionDenied"); +}); + +testPerm({ write: true }, function removeAllSyncDirSuccess() { + // REMOVE EMPTY DIRECTORY + let path = deno.makeTempDirSync() + "/dir/subdir"; + deno.mkdirSync(path); + let pathInfo = deno.statSync(path); + assert(pathInfo.isDirectory()); // check exist first + deno.removeAllSync(path); // remove + // We then check again after remove + let err; + try { + deno.statSync(path); + } catch (e) { + err = e; + } + // Directory is gone + assertEqual(err.kind, deno.ErrorKind.NotFound); + assertEqual(err.name, "NotFound"); + // REMOVE NON-EMPTY DIRECTORY + path = deno.makeTempDirSync() + "/dir/subdir"; + const subPath = path + "/subsubdir"; + deno.mkdirSync(path); + deno.mkdirSync(subPath); + pathInfo = deno.statSync(path); + assert(pathInfo.isDirectory()); // check exist first + const subPathInfo = deno.statSync(subPath); + assert(subPathInfo.isDirectory()); // check exist first + deno.removeAllSync(path); // remove + // We then check parent directory again after remove + try { + deno.statSync(path); + } catch (e) { + err = e; + } + // Directory is gone + assertEqual(err.kind, deno.ErrorKind.NotFound); + assertEqual(err.name, "NotFound"); +}); + +testPerm({ write: true }, function removeAllSyncFileSuccess() { + // REMOVE FILE + const enc = new TextEncoder(); + const data = enc.encode("Hello"); + const filename = deno.makeTempDirSync() + "/test.txt"; + deno.writeFileSync(filename, data, 0o666); + const fileInfo = deno.statSync(filename); + assert(fileInfo.isFile()); // check exist first + deno.removeAllSync(filename); // remove + // We then check again after remove + let err; + try { + deno.statSync(filename); + } catch (e) { + err = e; + } + // File is gone + assertEqual(err.kind, deno.ErrorKind.NotFound); + assertEqual(err.name, "NotFound"); +}); + +testPerm({ write: true }, function removeAllSyncFail() { + // NON-EXISTENT DIRECTORY/FILE + let err; + try { + // Non-existent + deno.removeAllSync("/baddir"); + } catch (e) { + err = e; + } + assertEqual(err.kind, deno.ErrorKind.NotFound); + assertEqual(err.name, "NotFound"); +}); + +testPerm({ write: false }, function removeAllSyncPerm() { + let err; + try { + deno.removeAllSync("/baddir"); + } catch (e) { + err = e; + } + assertEqual(err.kind, deno.ErrorKind.PermissionDenied); + assertEqual(err.name, "PermissionDenied"); +}); + +// ASYNC + +testPerm({ write: true }, async function removeDirSuccess() { + // REMOVE EMPTY DIRECTORY + const path = deno.makeTempDirSync() + "/dir/subdir"; + deno.mkdirSync(path); + const pathInfo = deno.statSync(path); + assert(pathInfo.isDirectory()); // check exist first + await deno.remove(path); // remove + // We then check again after remove + let err; + try { + deno.statSync(path); + } catch (e) { + err = e; + } + // Directory is gone + assertEqual(err.kind, deno.ErrorKind.NotFound); + assertEqual(err.name, "NotFound"); +}); + +testPerm({ write: true }, async function removeFileSuccess() { + // REMOVE FILE + const enc = new TextEncoder(); + const data = enc.encode("Hello"); + const filename = deno.makeTempDirSync() + "/test.txt"; + deno.writeFileSync(filename, data, 0o666); + const fileInfo = deno.statSync(filename); + assert(fileInfo.isFile()); // check exist first + await deno.remove(filename); // remove + // We then check again after remove + let err; + try { + deno.statSync(filename); + } catch (e) { + err = e; + } + // File is gone + assertEqual(err.kind, deno.ErrorKind.NotFound); + assertEqual(err.name, "NotFound"); +}); + +testPerm({ write: true }, async function removeFail() { + // NON-EMPTY DIRECTORY + const path = deno.makeTempDirSync() + "/dir/subdir"; + const subPath = path + "/subsubdir"; + deno.mkdirSync(path); + deno.mkdirSync(subPath); + const pathInfo = deno.statSync(path); + assert(pathInfo.isDirectory()); // check exist first + const subPathInfo = deno.statSync(subPath); + assert(subPathInfo.isDirectory()); // check exist first + let err; + try { + // Should not be able to recursively remove + await deno.remove(path); + } catch (e) { + err = e; + } + assertEqual(err.kind, deno.ErrorKind.Other); + assertEqual(err.name, "Other"); + // NON-EXISTENT DIRECTORY/FILE + try { + // Non-existent + await deno.remove("/baddir"); + } catch (e) { + err = e; + } + assertEqual(err.kind, deno.ErrorKind.NotFound); + assertEqual(err.name, "NotFound"); +}); + +testPerm({ write: false }, async function removePerm() { + let err; + try { + await deno.remove("/baddir"); + } catch (e) { + err = e; + } + assertEqual(err.kind, deno.ErrorKind.PermissionDenied); + assertEqual(err.name, "PermissionDenied"); +}); + +testPerm({ write: true }, async function removeAllDirSuccess() { + // REMOVE EMPTY DIRECTORY + let path = deno.makeTempDirSync() + "/dir/subdir"; + deno.mkdirSync(path); + let pathInfo = deno.statSync(path); + assert(pathInfo.isDirectory()); // check exist first + await deno.removeAll(path); // remove + // We then check again after remove + let err; + try { + deno.statSync(path); + } catch (e) { + err = e; + } + // Directory is gone + assertEqual(err.kind, deno.ErrorKind.NotFound); + assertEqual(err.name, "NotFound"); + // REMOVE NON-EMPTY DIRECTORY + path = deno.makeTempDirSync() + "/dir/subdir"; + const subPath = path + "/subsubdir"; + deno.mkdirSync(path); + deno.mkdirSync(subPath); + pathInfo = deno.statSync(path); + assert(pathInfo.isDirectory()); // check exist first + const subPathInfo = deno.statSync(subPath); + assert(subPathInfo.isDirectory()); // check exist first + await deno.removeAll(path); // remove + // We then check parent directory again after remove + try { + deno.statSync(path); + } catch (e) { + err = e; + } + // Directory is gone + assertEqual(err.kind, deno.ErrorKind.NotFound); + assertEqual(err.name, "NotFound"); +}); + +testPerm({ write: true }, async function removeAllFileSuccess() { + // REMOVE FILE + const enc = new TextEncoder(); + const data = enc.encode("Hello"); + const filename = deno.makeTempDirSync() + "/test.txt"; + deno.writeFileSync(filename, data, 0o666); + const fileInfo = deno.statSync(filename); + assert(fileInfo.isFile()); // check exist first + await deno.removeAll(filename); // remove + // We then check again after remove + let err; + try { + deno.statSync(filename); + } catch (e) { + err = e; + } + // File is gone + assertEqual(err.kind, deno.ErrorKind.NotFound); + assertEqual(err.name, "NotFound"); +}); + +testPerm({ write: true }, async function removeAllFail() { + // NON-EXISTENT DIRECTORY/FILE + let err; + try { + // Non-existent + await deno.removeAll("/baddir"); + } catch (e) { + err = e; + } + assertEqual(err.kind, deno.ErrorKind.NotFound); + assertEqual(err.name, "NotFound"); +}); + +testPerm({ write: false }, async function removeAllPerm() { + let err; + try { + await deno.removeAll("/baddir"); + } catch (e) { + err = e; + } + assertEqual(err.kind, deno.ErrorKind.PermissionDenied); + assertEqual(err.name, "PermissionDenied"); +}); diff --git a/js/stat_test.ts b/js/stat_test.ts index c811b23ab4..424077dc5a 100644 --- a/js/stat_test.ts +++ b/js/stat_test.ts @@ -123,4 +123,3 @@ test(async function lstatNotFound() { assert(caughtError); assertEqual(badInfo, undefined); }); - diff --git a/src/handlers.rs b/src/handlers.rs index 2ea00391e4..18152c682a 100644 --- a/src/handlers.rs +++ b/src/handlers.rs @@ -12,6 +12,7 @@ use hyper::Client; use libdeno; use libdeno::{deno_buf, DenoC}; use msg; +use remove_dir_all::remove_dir_all; use std; use std::fs; use std::path::Path; @@ -50,6 +51,7 @@ pub extern "C" fn msg_from_js(d: *const DenoC, buf: deno_buf) { msg::Any::TimerClear => handle_timer_clear, msg::Any::MakeTempDir => handle_make_temp_dir, msg::Any::Mkdir => handle_mkdir, + msg::Any::Remove => handle_remove, msg::Any::ReadFile => handle_read_file, msg::Any::RenameSync => handle_rename_sync, msg::Any::SetEnv => handle_set_env, @@ -435,6 +437,32 @@ fn handle_mkdir(d: *const DenoC, base: &msg::Base) -> Box { }())) } +fn handle_remove(d: *const DenoC, base: &msg::Base) -> Box { + let msg = base.msg_as_remove().unwrap(); + let path = msg.path().unwrap(); + let recursive = msg.recursive(); + let deno = from_c(d); + if !deno.flags.allow_write { + return odd_future(permission_denied()); + } + // TODO Use tokio_threadpool. + Box::new(futures::future::result(|| -> OpResult { + debug!("handle_remove {}", path); + let path_ = Path::new(&path); + let metadata = fs::metadata(&path_)?; + if metadata.is_file() { + fs::remove_file(&path_)?; + } else { + if recursive { + remove_dir_all(&path_)?; + } else { + fs::remove_dir(&path_)?; + } + } + Ok(None) + }())) +} + // Prototype https://github.com/denoland/deno/blob/golang/os.go#L171-L184 fn handle_read_file(_d: *const DenoC, base: &msg::Base) -> Box { let msg = base.msg_as_read_file().unwrap(); diff --git a/src/main.rs b/src/main.rs index 68e680c719..12179ac6e3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -10,6 +10,7 @@ extern crate url; #[macro_use] extern crate log; extern crate hyper_rustls; +extern crate remove_dir_all; extern crate ring; mod deno_dir; diff --git a/src/msg.fbs b/src/msg.fbs index f988c9d0bb..4437ad1bca 100644 --- a/src/msg.fbs +++ b/src/msg.fbs @@ -15,6 +15,7 @@ union Any { MakeTempDir, MakeTempDirRes, Mkdir, + Remove, ReadFile, ReadFileRes, WriteFile, @@ -173,6 +174,11 @@ table Mkdir { // mode specified by https://godoc.org/os#FileMode } +table Remove { + path: string; + recursive: bool; +} + table ReadFile { filename: string; }