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

Add link/linkSync fs call for hardlinks #2074

Merged
merged 1 commit into from Apr 8, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions cli/BUILD.gn
Expand Up @@ -72,6 +72,7 @@ ts_sources = [
"../js/headers.ts",
"../js/io.ts",
"../js/lib.web_assembly.d.ts",
"../js/link.ts",
"../js/location.ts",
"../js/main.ts",
"../js/make_temp_dir.ts",
Expand Down
6 changes: 6 additions & 0 deletions cli/msg.fbs
Expand Up @@ -21,6 +21,7 @@ union Any {
GlobalTimerStop,
IsTTY,
IsTTYRes,
Link,
Listen,
ListenRes,
MakeTempDir,
Expand Down Expand Up @@ -391,6 +392,11 @@ table Symlink {
newname: string;
}

table Link {
oldname: string;
newname: string;
}

table Stat {
filename: string;
lstat: bool;
Expand Down
23 changes: 23 additions & 0 deletions cli/ops.rs
Expand Up @@ -176,6 +176,7 @@ pub fn op_selector_std(inner_type: msg::Any) -> Option<OpCreator> {
msg::Any::GlobalTimer => Some(op_global_timer),
msg::Any::GlobalTimerStop => Some(op_global_timer_stop),
msg::Any::IsTTY => Some(op_is_tty),
msg::Any::Link => Some(op_link),
msg::Any::Listen => Some(op_listen),
msg::Any::MakeTempDir => Some(op_make_temp_dir),
msg::Any::Metrics => Some(op_metrics),
Expand Down Expand Up @@ -1259,6 +1260,28 @@ fn op_rename(
})
}

fn op_link(
sc: &IsolateStateContainer,
base: &msg::Base<'_>,
data: deno_buf,
) -> Box<OpWithError> {
assert_eq!(data.len(), 0);
let inner = base.inner_as_link().unwrap();
let oldname = PathBuf::from(inner.oldname().unwrap());
let newname_ = inner.newname().unwrap();
let newname = PathBuf::from(newname_);

if let Err(e) = sc.state().check_write(&newname_) {
return odd_future(e);
}

blocking(base.sync(), move || -> OpResult {
debug!("op_link {} {}", oldname.display(), newname.display());
std::fs::hard_link(&oldname, &newname)?;
Ok(empty_buf())
})
}

fn op_symlink(
sc: &IsolateStateContainer,
base: &msg::Base<'_>,
Expand Down
1 change: 1 addition & 0 deletions js/deno.ts
Expand Up @@ -53,6 +53,7 @@ export { readDirSync, readDir } from "./read_dir";
export { copyFileSync, copyFile } from "./copy_file";
export { readlinkSync, readlink } from "./read_link";
export { statSync, lstatSync, stat, lstat } from "./stat";
export { linkSync, link } from "./link";
export { symlinkSync, symlink } from "./symlink";
export { writeFileSync, writeFile, WriteFileOptions } from "./write_file";
export { ErrorKind, DenoError } from "./errors";
Expand Down
31 changes: 31 additions & 0 deletions js/link.ts
@@ -0,0 +1,31 @@
// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
import * as msg from "gen/cli/msg_generated";
import * as flatbuffers from "./flatbuffers";
import * as dispatch from "./dispatch";

function req(
oldname: string,
newname: string
): [flatbuffers.Builder, msg.Any, flatbuffers.Offset] {
const builder = flatbuffers.createBuilder();
const oldname_ = builder.createString(oldname);
const newname_ = builder.createString(newname);
const inner = msg.Link.createLink(builder, oldname_, newname_);
return [builder, msg.Any.Link, inner];
}

/** Synchronously creates `newname` as a hard link to `oldname`.
*
* Deno.linkSync("old/name", "new/name");
*/
export function linkSync(oldname: string, newname: string): void {
dispatch.sendSync(...req(oldname, newname));
}

/** Creates `newname` as a hard link to `oldname`.
*
* await Deno.link("old/name", "new/name");
*/
export async function link(oldname: string, newname: string): Promise<void> {
await dispatch.sendAsync(...req(oldname, newname));
}
104 changes: 104 additions & 0 deletions js/link_test.ts
@@ -0,0 +1,104 @@
// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
import { test, testPerm, assert, assertEquals } from "./test_util.ts";

testPerm({ read: true, write: true }, function linkSyncSuccess() {
const testDir = Deno.makeTempDirSync();
const oldData = "Hardlink";
const oldName = testDir + "/oldname";
const newName = testDir + "/newname";
Deno.writeFileSync(oldName, new TextEncoder().encode(oldData));
// Create the hard link.
Deno.linkSync(oldName, newName);
// We should expect reading the same content.
const newData = new TextDecoder().decode(Deno.readFileSync(newName));
assertEquals(oldData, newData);
// Writing to newname also affects oldname.
const newData2 = "Modified";
Deno.writeFileSync(newName, new TextEncoder().encode(newData2));
assertEquals(newData2, new TextDecoder().decode(Deno.readFileSync(oldName)));
// Writing to oldname also affects newname.
const newData3 = "ModifiedAgain";
Deno.writeFileSync(oldName, new TextEncoder().encode(newData3));
assertEquals(newData3, new TextDecoder().decode(Deno.readFileSync(newName)));
// Remove oldname. File still accessible through newname.
Deno.removeSync(oldName);
const newNameStat = Deno.statSync(newName);
assert(newNameStat.isFile());
assert(!newNameStat.isSymlink()); // Not a symlink.
assertEquals(newData3, new TextDecoder().decode(Deno.readFileSync(newName)));
});

testPerm({ read: true, write: true }, function linkSyncExists() {
const testDir = Deno.makeTempDirSync();
const oldName = testDir + "/oldname";
const newName = testDir + "/newname";
Deno.writeFileSync(oldName, new TextEncoder().encode("oldName"));
// newname is already created.
Deno.writeFileSync(newName, new TextEncoder().encode("newName"));

let err;
try {
Deno.linkSync(oldName, newName);
} catch (e) {
err = e;
}
assert(!!err);
console.log(err);
assertEquals(err.kind, Deno.ErrorKind.AlreadyExists);
assertEquals(err.name, "AlreadyExists");
});

testPerm({ read: true, write: true }, function linkSyncNotFound() {
const testDir = Deno.makeTempDirSync();
const oldName = testDir + "/oldname";
const newName = testDir + "/newname";

let err;
try {
Deno.linkSync(oldName, newName);
} catch (e) {
err = e;
}
assert(!!err);
console.log(err);
assertEquals(err.kind, Deno.ErrorKind.NotFound);
assertEquals(err.name, "NotFound");
});

test(function linkSyncPerm() {
let err;
try {
Deno.linkSync("oldbaddir", "newbaddir");
} catch (e) {
err = e;
}
assertEquals(err.kind, Deno.ErrorKind.PermissionDenied);
assertEquals(err.name, "PermissionDenied");
});

testPerm({ read: true, write: true }, async function linkSuccess() {
const testDir = Deno.makeTempDirSync();
const oldData = "Hardlink";
const oldName = testDir + "/oldname";
const newName = testDir + "/newname";
Deno.writeFileSync(oldName, new TextEncoder().encode(oldData));
// Create the hard link.
await Deno.link(oldName, newName);
// We should expect reading the same content.
const newData = new TextDecoder().decode(Deno.readFileSync(newName));
assertEquals(oldData, newData);
// Writing to newname also affects oldname.
const newData2 = "Modified";
Deno.writeFileSync(newName, new TextEncoder().encode(newData2));
assertEquals(newData2, new TextDecoder().decode(Deno.readFileSync(oldName)));
// Writing to oldname also affects newname.
const newData3 = "ModifiedAgain";
Deno.writeFileSync(oldName, new TextEncoder().encode(newData3));
assertEquals(newData3, new TextDecoder().decode(Deno.readFileSync(newName)));
// Remove oldname. File still accessible through newname.
Deno.removeSync(oldName);
const newNameStat = Deno.statSync(newName);
assert(newNameStat.isFile());
assert(!newNameStat.isSymlink()); // Not a symlink.
assertEquals(newData3, new TextDecoder().decode(Deno.readFileSync(newName)));
});
1 change: 1 addition & 0 deletions js/unit_tests.ts
Expand Up @@ -23,6 +23,7 @@ import "./files_test.ts";
import "./form_data_test.ts";
import "./globals_test.ts";
import "./headers_test.ts";
import "./link_test.ts";
import "./location_test.ts";
import "./make_temp_dir_test.ts";
import "./metrics_test.ts";
Expand Down