aboutsummaryrefslogtreecommitdiff
path: root/src/bun.js/node
diff options
context:
space:
mode:
authorGravatar dave caruso <me@paperdave.net> 2023-08-30 18:30:06 -0700
committerGravatar GitHub <noreply@github.com> 2023-08-30 18:30:06 -0700
commit0a5d2a8195fbbaab7ff1f40ad54ba94726bcc104 (patch)
tree218b52f72b1170c8595800dcda34adc312adf08b /src/bun.js/node
parent89f24e66fff37eab4b984847b73be0b69dbcd0a8 (diff)
downloadbun-0a5d2a8195fbbaab7ff1f40ad54ba94726bcc104.tar.gz
bun-0a5d2a8195fbbaab7ff1f40ad54ba94726bcc104.tar.zst
bun-0a5d2a8195fbbaab7ff1f40ad54ba94726bcc104.zip
feat(node:fs): add `cp`/`cpSync`/`promises.cp` + async `copyFile` (#4340)
* half working disaster code * this * async copyFile * . * its failing symlink tests * asdfg * asdf * hmm * okay i think ti works * small edits * fix test on linux * i hate atomics / atomics hate me back <3 * add a message in the builtins bundler that 0.8 is needed. it breaks on older versions lol. * fixed * rebase
Diffstat (limited to 'src/bun.js/node')
-rw-r--r--src/bun.js/node/node.classes.ts6
-rw-r--r--src/bun.js/node/node_fs.zig1199
-rw-r--r--src/bun.js/node/node_fs_binding.zig19
3 files changed, 1056 insertions, 168 deletions
diff --git a/src/bun.js/node/node.classes.ts b/src/bun.js/node/node.classes.ts
index a8d2e08d7..695a58d5e 100644
--- a/src/bun.js/node/node.classes.ts
+++ b/src/bun.js/node/node.classes.ts
@@ -410,8 +410,8 @@ export default [
copyFileSync: { fn: "copyFileSync", length: 3 },
// TODO:
- // cp: { fn: "cp", length: 4 },
- // cpSync: { fn: "cpSync", length: 3 },
+ cp: { fn: "cp", length: 2 },
+ cpSync: { fn: "cpSync", length: 2 },
exists: { fn: "exists", length: 2 },
existsSync: { fn: "existsSync", length: 1 },
@@ -477,8 +477,8 @@ export default [
unlinkSync: { fn: "unlinkSync", length: 1 },
utimes: { fn: "utimes", length: 4 },
utimesSync: { fn: "utimesSync", length: 3 },
- // TODO:
watch: { fn: "watch", length: 3 },
+ // TODO:
// watchFile: { fn: "watchFile", length: 3 },
writeFile: { fn: "writeFile", length: 4 },
writeFileSync: { fn: "writeFileSync", length: 3 },
diff --git a/src/bun.js/node/node_fs.zig b/src/bun.js/node/node_fs.zig
index 2750bd1b8..8f822448b 100644
--- a/src/bun.js/node/node_fs.zig
+++ b/src/bun.js/node/node_fs.zig
@@ -419,6 +419,266 @@ pub const AsyncReadFileTask = struct {
}
};
+pub const AsyncCopyFileTask = struct {
+ promise: JSC.JSPromise.Strong,
+ args: Arguments.CopyFile,
+ globalObject: *JSC.JSGlobalObject,
+ task: JSC.WorkPoolTask = .{ .callback = &workPoolCallback },
+ result: JSC.Maybe(Return.CopyFile),
+ ref: JSC.PollRef = .{},
+ arena: bun.ArenaAllocator,
+ tracker: JSC.AsyncTaskTracker,
+
+ pub fn create(
+ globalObject: *JSC.JSGlobalObject,
+ copyfile_args: Arguments.CopyFile,
+ vm: *JSC.VirtualMachine,
+ arena: bun.ArenaAllocator,
+ ) JSC.JSValue {
+ var task = bun.default_allocator.create(AsyncCopyFileTask) catch @panic("out of memory");
+ task.* = AsyncCopyFileTask{
+ .promise = JSC.JSPromise.Strong.init(globalObject),
+ .args = copyfile_args,
+ .result = undefined,
+ .globalObject = globalObject,
+ .tracker = JSC.AsyncTaskTracker.init(vm),
+ .arena = arena,
+ };
+ task.ref.ref(vm);
+ task.args.src.toThreadSafe();
+ task.args.dest.toThreadSafe();
+ task.tracker.didSchedule(globalObject);
+
+ JSC.WorkPool.schedule(&task.task);
+
+ return task.promise.value();
+ }
+
+ fn workPoolCallback(task: *JSC.WorkPoolTask) void {
+ var this: *AsyncCopyFileTask = @fieldParentPtr(AsyncCopyFileTask, "task", task);
+
+ var node_fs = NodeFS{};
+ this.result = node_fs.copyFile(this.args, .promise);
+
+ this.globalObject.bunVMConcurrently().eventLoop().enqueueTaskConcurrent(JSC.ConcurrentTask.fromCallback(this, runFromJSThread));
+ }
+
+ fn runFromJSThread(this: *AsyncCopyFileTask) void {
+ var globalObject = this.globalObject;
+ var success = @as(JSC.Maybe(Return.CopyFile).Tag, this.result) == .result;
+ const result = switch (this.result) {
+ .err => |err| err.toJSC(globalObject),
+ .result => |res| brk: {
+ var exceptionref: JSC.C.JSValueRef = null;
+ const out = JSC.JSValue.c(JSC.To.JS.withType(Return.CopyFile, res, globalObject, &exceptionref));
+ const exception = JSC.JSValue.c(exceptionref);
+ if (exception != .zero) {
+ success = false;
+ break :brk exception;
+ }
+
+ break :brk out;
+ },
+ };
+ var promise_value = this.promise.value();
+ var promise = this.promise.get();
+ promise_value.ensureStillAlive();
+
+ const tracker = this.tracker;
+ tracker.willDispatch(globalObject);
+ defer tracker.didDispatch(globalObject);
+
+ this.deinit();
+ switch (success) {
+ false => {
+ promise.reject(globalObject, result);
+ },
+ true => {
+ promise.resolve(globalObject, result);
+ },
+ }
+ }
+
+ pub fn deinit(this: *AsyncCopyFileTask) void {
+ this.ref.unref(this.globalObject.bunVM());
+ this.args.deinit();
+ this.promise.strong.deinit();
+ this.arena.deinit();
+ bun.default_allocator.destroy(this);
+ }
+};
+
+pub const AsyncCpTask = struct {
+ promise: JSC.JSPromise.Strong,
+ args: Arguments.Cp,
+ globalObject: *JSC.JSGlobalObject,
+ task: JSC.WorkPoolTask = .{ .callback = &workPoolCallback },
+ result: JSC.Maybe(Return.Cp),
+ ref: JSC.PollRef = .{},
+ arena: bun.ArenaAllocator,
+ tracker: JSC.AsyncTaskTracker,
+ has_result: std.atomic.Atomic(bool),
+ /// On each creation of a `AsyncCpSingleFileTask`, this is incremented.
+ /// When each task is finished, decrement.
+ /// The maintask thread starts this at 1 and decrements it at the end, to avoid the promise being resolved while new tasks may be added.
+ subtask_count: std.atomic.Atomic(usize),
+
+ pub fn create(
+ globalObject: *JSC.JSGlobalObject,
+ cp_args: Arguments.Cp,
+ vm: *JSC.VirtualMachine,
+ arena: bun.ArenaAllocator,
+ ) JSC.JSValue {
+ var task = bun.default_allocator.create(AsyncCpTask) catch @panic("out of memory");
+ task.* = AsyncCpTask{
+ .promise = JSC.JSPromise.Strong.init(globalObject),
+ .args = cp_args,
+ .has_result = .{ .value = false },
+ .result = undefined,
+ .globalObject = globalObject,
+ .tracker = JSC.AsyncTaskTracker.init(vm),
+ .arena = arena,
+ .subtask_count = .{ .value = 1 },
+ };
+ task.ref.ref(vm);
+ task.args.src.toThreadSafe();
+ task.args.dest.toThreadSafe();
+ task.tracker.didSchedule(globalObject);
+
+ JSC.WorkPool.schedule(&task.task);
+
+ return task.promise.value();
+ }
+
+ fn workPoolCallback(task: *JSC.WorkPoolTask) void {
+ var this: *AsyncCpTask = @fieldParentPtr(AsyncCpTask, "task", task);
+
+ var node_fs = NodeFS{};
+ node_fs.cpAsync(this);
+ }
+
+ /// May be called from any thread (the subtasks)
+ fn finishConcurrently(this: *AsyncCpTask, result: Maybe(Return.Cp)) void {
+ if (this.has_result.compareAndSwap(false, true, .Monotonic, .Monotonic)) |_| {
+ return;
+ }
+
+ this.result = result;
+ this.globalObject.bunVMConcurrently().eventLoop().enqueueTaskConcurrent(JSC.ConcurrentTask.fromCallback(this, runFromJSThread));
+ }
+
+ fn runFromJSThread(this: *AsyncCpTask) void {
+ var globalObject = this.globalObject;
+ var success = @as(JSC.Maybe(Return.Cp).Tag, this.result) == .result;
+ const result = switch (this.result) {
+ .err => |err| err.toJSC(globalObject),
+ .result => |res| brk: {
+ var exceptionref: JSC.C.JSValueRef = null;
+ const out = JSC.JSValue.c(JSC.To.JS.withType(Return.Cp, res, globalObject, &exceptionref));
+ const exception = JSC.JSValue.c(exceptionref);
+ if (exception != .zero) {
+ success = false;
+ break :brk exception;
+ }
+
+ break :brk out;
+ },
+ };
+ var promise_value = this.promise.value();
+ var promise = this.promise.get();
+ promise_value.ensureStillAlive();
+
+ const tracker = this.tracker;
+ tracker.willDispatch(globalObject);
+ defer tracker.didDispatch(globalObject);
+
+ this.deinit();
+ switch (success) {
+ false => {
+ promise.reject(globalObject, result);
+ },
+ true => {
+ promise.resolve(globalObject, result);
+ },
+ }
+ }
+
+ pub fn deinit(this: *AsyncCpTask) void {
+ this.ref.unref(this.globalObject.bunVM());
+ this.args.deinit();
+ this.promise.strong.deinit();
+ this.arena.deinit();
+ bun.default_allocator.destroy(this);
+ }
+};
+
+/// This task is used by `AsyncCpTask/fs.promises.cp` to copy a single file.
+/// When clonefile cannot be used, this task is started once per file.
+pub const AsyncCpSingleFileTask = struct {
+ cp_task: *AsyncCpTask,
+ src: [:0]const u8,
+ dest: [:0]const u8,
+ task: JSC.WorkPoolTask = .{ .callback = &workPoolCallback },
+
+ pub fn create(
+ parent: *AsyncCpTask,
+ src: [:0]const u8,
+ dest: [:0]const u8,
+ ) void {
+ var task = bun.default_allocator.create(AsyncCpSingleFileTask) catch @panic("out of memory");
+ task.* = AsyncCpSingleFileTask{
+ .cp_task = parent,
+ .src = src,
+ .dest = dest,
+ };
+
+ JSC.WorkPool.schedule(&task.task);
+ }
+
+ fn workPoolCallback(task: *JSC.WorkPoolTask) void {
+ var this: *AsyncCpSingleFileTask = @fieldParentPtr(AsyncCpSingleFileTask, "task", task);
+
+ // TODO: error strings on node_fs will die
+ var node_fs = NodeFS{};
+
+ const args = this.cp_task.args;
+ const result = node_fs._copySingleFileSync(
+ this.src,
+ this.dest,
+ @enumFromInt((if (args.flags.errorOnExist or !args.flags.force) Constants.COPYFILE_EXCL else @as(u8, 0))),
+ null,
+ );
+
+ brk: {
+ switch (result) {
+ .err => |err| {
+ if (err.errno == @intFromEnum(os.E.EXIST) and !args.flags.errorOnExist) {
+ break :brk;
+ }
+ this.cp_task.finishConcurrently(result);
+ this.deinit();
+ return;
+ },
+ .result => {},
+ }
+ }
+
+ const old_count = this.cp_task.subtask_count.fetchSub(1, .Monotonic);
+ if (old_count == 1) {
+ this.cp_task.finishConcurrently(Maybe(Return.Cp).success);
+ }
+
+ this.deinit();
+ }
+
+ pub fn deinit(this: *AsyncCpSingleFileTask) void {
+ // There is only one path buffer for both paths. 2 extra bytes are the nulls at the end of each
+ bun.default_allocator.free(this.src.ptr[0 .. this.src.len + this.dest.len + 2]);
+
+ bun.default_allocator.destroy(this);
+ }
+};
+
// TODO: to improve performance for all of these
// The tagged unions for each type should become regular unions
// and the tags should be passed in as comptime arguments to the functions performing the syscalls
@@ -2807,7 +3067,93 @@ pub const Arguments = struct {
return CopyFile{
.src = src,
.dest = dest,
- .mode = @as(Constants.Copyfile, @enumFromInt(mode)),
+ .mode = @enumFromInt(mode),
+ };
+ }
+ };
+
+ pub const Cp = struct {
+ src: PathLike,
+ dest: PathLike,
+ flags: Flags,
+
+ const Flags = struct {
+ mode: Constants.Copyfile,
+ recursive: bool,
+ errorOnExist: bool,
+ force: bool,
+ };
+
+ fn deinit(this: Cp) void {
+ this.src.deinit();
+ this.dest.deinit();
+ }
+
+ pub fn fromJS(ctx: JSC.C.JSContextRef, arguments: *ArgumentsSlice, exception: JSC.C.ExceptionRef) ?Cp {
+ const src = PathLike.fromJS(ctx, arguments, exception) orelse {
+ if (exception.* == null) {
+ JSC.throwInvalidArguments(
+ "src must be a string or buffer",
+ .{},
+ ctx,
+ exception,
+ );
+ }
+ return null;
+ };
+
+ if (exception.* != null) return null;
+
+ const dest = PathLike.fromJS(ctx, arguments, exception) orelse {
+ if (exception.* == null) {
+ JSC.throwInvalidArguments(
+ "dest must be a string or buffer",
+ .{},
+ ctx,
+ exception,
+ );
+ }
+ return null;
+ };
+
+ if (exception.* != null) return null;
+
+ var recursive: bool = false;
+ var errorOnExist: bool = false;
+ var force: bool = true;
+ var mode: i32 = 0;
+
+ if (arguments.next()) |arg| {
+ arguments.eat();
+ recursive = arg.asBoolean();
+ }
+
+ if (arguments.next()) |arg| {
+ arguments.eat();
+ errorOnExist = arg.asBoolean();
+ }
+
+ if (arguments.next()) |arg| {
+ arguments.eat();
+ force = arg.asBoolean();
+ }
+
+ if (arguments.next()) |arg| {
+ arguments.eat();
+ if (arg.isNumber()) {
+ mode = arg.coerce(i32, ctx);
+ }
+ }
+
+ return Cp{
+ .src = src,
+ .dest = dest,
+ .flags = .{
+ .mode = @enumFromInt(mode),
+ .recursive = recursive,
+ .errorOnExist = errorOnExist,
+ .force = force,
+ },
};
}
};
@@ -2824,37 +3170,6 @@ pub const Arguments = struct {
position: ReadPosition,
};
- pub const Copy = struct {
- pub const FilterCallback = *const fn (source: string, destination: string) bool;
- /// Dereference symlinks
- /// @default false
- dereference: bool = false,
-
- /// When `force` is `false`, and the destination
- /// exists, throw an error.
- /// @default false
- errorOnExist: bool = false,
-
- /// Function to filter copied files/directories. Return
- /// `true` to copy the item, `false` to ignore it.
- filter: ?FilterCallback = null,
-
- /// Overwrite existing file or directory. _The copy
- /// operation will ignore errors if you set this to false and the destination
- /// exists. Use the `errorOnExist` option to change this behavior.
- /// @default true
- force: bool = true,
-
- /// When `true` timestamps from `src` will
- /// be preserved.
- /// @default false
- preserve_timestamps: bool = false,
-
- /// Copy directories recursively.
- /// @default false
- recursive: bool = false,
- };
-
pub const UnwatchFile = void;
pub const Watch = JSC.Node.FSWatcher.Arguments;
pub const WatchFile = void;
@@ -2910,6 +3225,7 @@ const Return = struct {
pub const AppendFile = void;
pub const Close = void;
pub const CopyFile = void;
+ pub const Cp = void;
pub const Exists = bool;
pub const Fchmod = void;
pub const Chmod = void;
@@ -3216,105 +3532,50 @@ pub const NodeFS = struct {
/// https://github.com/libuv/libuv/pull/2578
/// https://github.com/nodejs/node/issues/34624
pub fn copyFile(_: *NodeFS, args: Arguments.CopyFile, comptime flavor: Flavor) Maybe(Return.CopyFile) {
+ _ = flavor;
const ret = Maybe(Return.CopyFile);
- switch (comptime flavor) {
- .sync => {
- var src_buf: [bun.MAX_PATH_BYTES]u8 = undefined;
- var dest_buf: [bun.MAX_PATH_BYTES]u8 = undefined;
- var src = args.src.sliceZ(&src_buf);
- var dest = args.dest.sliceZ(&dest_buf);
-
- // TODO: do we need to fchown?
- if (comptime Environment.isMac) {
- if (args.mode.isForceClone()) {
- // https://www.manpagez.com/man/2/clonefile/
- return ret.errnoSysP(C.clonefile(src, dest, 0), .clonefile, src) orelse ret.success;
- } else {
- const stat_ = switch (Syscall.stat(src)) {
- .result => |result| result,
- .err => |err| return Maybe(Return.CopyFile){ .err = err.withPath(src) },
- };
-
- if (!os.S.ISREG(stat_.mode)) {
- return Maybe(Return.CopyFile){ .err = .{ .errno = @intFromEnum(C.SystemErrno.ENOTSUP) } };
- }
+ var src_buf: [bun.MAX_PATH_BYTES]u8 = undefined;
+ var dest_buf: [bun.MAX_PATH_BYTES]u8 = undefined;
+ var src = args.src.sliceZ(&src_buf);
+ var dest = args.dest.sliceZ(&dest_buf);
- // 64 KB is about the break-even point for clonefile() to be worth it
- // at least, on an M1 with an NVME SSD.
- if (stat_.size > 128 * 1024) {
- if (!args.mode.shouldntOverwrite()) {
- // clonefile() will fail if it already exists
- _ = Syscall.unlink(dest);
- }
-
- if (ret.errnoSysP(C.clonefile(src, dest, 0), .clonefile, src) == null) {
- _ = C.chmod(dest, stat_.mode);
- return ret.success;
- }
- } else {
- const src_fd = switch (Syscall.open(src, std.os.O.RDONLY, 0o644)) {
- .result => |result| result,
- .err => |err| return .{ .err = err.withPath(args.src.slice()) },
- };
- defer {
- _ = Syscall.close(src_fd);
- }
-
- var flags: Mode = std.os.O.CREAT | std.os.O.WRONLY;
- var wrote: usize = 0;
- if (args.mode.shouldntOverwrite()) {
- flags |= std.os.O.EXCL;
- }
-
- const dest_fd = switch (Syscall.open(dest, flags, JSC.Node.default_permission)) {
- .result => |result| result,
- .err => |err| return Maybe(Return.CopyFile){ .err = err },
- };
- defer {
- _ = std.c.ftruncate(dest_fd, @as(std.c.off_t, @intCast(@as(u63, @truncate(wrote)))));
- _ = C.fchmod(dest_fd, stat_.mode);
- _ = Syscall.close(dest_fd);
- }
-
- return copyFileUsingReadWriteLoop(src, dest, src_fd, dest_fd, @intCast(@max(stat_.size, 0)), &wrote);
- }
- }
-
- // we fallback to copyfile() when the file is > 128 KB and clonefile fails
- // clonefile() isn't supported on all devices
- // nor is it supported across devices
- var mode: Mode = C.darwin.COPYFILE_ACL | C.darwin.COPYFILE_DATA;
- if (args.mode.shouldntOverwrite()) {
- mode |= C.darwin.COPYFILE_EXCL;
- }
+ // TODO: do we need to fchown?
+ if (comptime Environment.isMac) {
+ if (args.mode.isForceClone()) {
+ // https://www.manpagez.com/man/2/clonefile/
+ return ret.errnoSysP(C.clonefile(src, dest, 0), .clonefile, src) orelse ret.success;
+ } else {
+ const stat_ = switch (Syscall.stat(src)) {
+ .result => |result| result,
+ .err => |err| return Maybe(Return.CopyFile){ .err = err.withPath(src) },
+ };
- return ret.errnoSysP(C.copyfile(src, dest, null, mode), .copyfile, src) orelse ret.success;
+ if (!os.S.ISREG(stat_.mode)) {
+ return Maybe(Return.CopyFile){ .err = .{ .errno = @intFromEnum(C.SystemErrno.ENOTSUP) } };
}
- if (comptime Environment.isLinux) {
- // https://manpages.debian.org/testing/manpages-dev/ioctl_ficlone.2.en.html
- if (args.mode.isForceClone()) {
- return Maybe(Return.CopyFile).todo;
+ // 64 KB is about the break-even point for clonefile() to be worth it
+ // at least, on an M1 with an NVME SSD.
+ if (stat_.size > 128 * 1024) {
+ if (!args.mode.shouldntOverwrite()) {
+ // clonefile() will fail if it already exists
+ _ = Syscall.unlink(dest);
}
+ if (ret.errnoSysP(C.clonefile(src, dest, 0), .clonefile, src) == null) {
+ _ = C.chmod(dest, stat_.mode);
+ return ret.success;
+ }
+ } else {
const src_fd = switch (Syscall.open(src, std.os.O.RDONLY, 0o644)) {
.result => |result| result,
- .err => |err| return .{ .err = err },
+ .err => |err| return .{ .err = err.withPath(args.src.slice()) },
};
defer {
_ = Syscall.close(src_fd);
}
- const stat_: linux.Stat = switch (Syscall.fstat(src_fd)) {
- .result => |result| result,
- .err => |err| return Maybe(Return.CopyFile){ .err = err },
- };
-
- if (!os.S.ISREG(stat_.mode)) {
- return Maybe(Return.CopyFile){ .err = .{ .errno = @intFromEnum(C.SystemErrno.ENOTSUP) } };
- }
-
var flags: Mode = std.os.O.CREAT | std.os.O.WRONLY;
var wrote: usize = 0;
if (args.mode.shouldntOverwrite()) {
@@ -3325,63 +3586,113 @@ pub const NodeFS = struct {
.result => |result| result,
.err => |err| return Maybe(Return.CopyFile){ .err = err },
};
-
- var size: usize = @intCast(@max(stat_.size, 0));
-
defer {
- _ = linux.ftruncate(dest_fd, @as(i64, @intCast(@as(u63, @truncate(wrote)))));
- _ = linux.fchmod(dest_fd, stat_.mode);
+ _ = std.c.ftruncate(dest_fd, @as(std.c.off_t, @intCast(@as(u63, @truncate(wrote)))));
+ _ = C.fchmod(dest_fd, stat_.mode);
_ = Syscall.close(dest_fd);
}
- var off_in_copy = @as(i64, @bitCast(@as(u64, 0)));
- var off_out_copy = @as(i64, @bitCast(@as(u64, 0)));
+ return copyFileUsingReadWriteLoop(src, dest, src_fd, dest_fd, @intCast(@max(stat_.size, 0)), &wrote);
+ }
+ }
- if (!bun.canUseCopyFileRangeSyscall()) {
- return copyFileUsingReadWriteLoop(src, dest, src_fd, dest_fd, size, &wrote);
- }
+ // we fallback to copyfile() when the file is > 128 KB and clonefile fails
+ // clonefile() isn't supported on all devices
+ // nor is it supported across devices
+ var mode: Mode = C.darwin.COPYFILE_ACL | C.darwin.COPYFILE_DATA;
+ if (args.mode.shouldntOverwrite()) {
+ mode |= C.darwin.COPYFILE_EXCL;
+ }
- if (size == 0) {
- // copy until EOF
- while (true) {
+ return ret.errnoSysP(C.copyfile(src, dest, null, mode), .copyfile, src) orelse ret.success;
+ }
- // Linux Kernel 5.3 or later
- const written = linux.copy_file_range(src_fd, &off_in_copy, dest_fd, &off_out_copy, std.mem.page_size, 0);
- if (ret.errnoSysP(written, .copy_file_range, dest)) |err| {
- return switch (err.getErrno()) {
- .XDEV, .NOSYS => copyFileUsingReadWriteLoop(src, dest, src_fd, dest_fd, size, &wrote),
- else => return err,
- };
- }
- // wrote zero bytes means EOF
- if (written == 0) break;
- wrote +|= written;
- }
- } else {
- while (size > 0) {
- // Linux Kernel 5.3 or later
- const written = linux.copy_file_range(src_fd, &off_in_copy, dest_fd, &off_out_copy, size, 0);
- if (ret.errnoSysP(written, .copy_file_range, dest)) |err| {
- return switch (err.getErrno()) {
- .XDEV, .NOSYS => copyFileUsingReadWriteLoop(src, dest, src_fd, dest_fd, size, &wrote),
- else => return err,
- };
- }
- // wrote zero bytes means EOF
- if (written == 0) break;
- wrote +|= written;
- size -|= written;
- }
- }
+ if (comptime Environment.isLinux) {
+ // https://manpages.debian.org/testing/manpages-dev/ioctl_ficlone.2.en.html
+ if (args.mode.isForceClone()) {
+ return Maybe(Return.CopyFile).todo;
+ }
+
+ const src_fd = switch (Syscall.open(src, std.os.O.RDONLY, 0o644)) {
+ .result => |result| result,
+ .err => |err| return .{ .err = err },
+ };
+ defer {
+ _ = Syscall.close(src_fd);
+ }
+
+ const stat_: linux.Stat = switch (Syscall.fstat(src_fd)) {
+ .result => |result| result,
+ .err => |err| return Maybe(Return.CopyFile){ .err = err },
+ };
+
+ if (!os.S.ISREG(stat_.mode)) {
+ return Maybe(Return.CopyFile){ .err = .{ .errno = @intFromEnum(C.SystemErrno.ENOTSUP) } };
+ }
+
+ var flags: Mode = std.os.O.CREAT | std.os.O.WRONLY;
+ var wrote: usize = 0;
+ if (args.mode.shouldntOverwrite()) {
+ flags |= std.os.O.EXCL;
+ }
+
+ const dest_fd = switch (Syscall.open(dest, flags, JSC.Node.default_permission)) {
+ .result => |result| result,
+ .err => |err| return Maybe(Return.CopyFile){ .err = err },
+ };
+
+ var size: usize = @intCast(@max(stat_.size, 0));
+
+ defer {
+ _ = linux.ftruncate(dest_fd, @as(i64, @intCast(@as(u63, @truncate(wrote)))));
+ _ = linux.fchmod(dest_fd, stat_.mode);
+ _ = Syscall.close(dest_fd);
+ }
+
+ var off_in_copy = @as(i64, @bitCast(@as(u64, 0)));
+ var off_out_copy = @as(i64, @bitCast(@as(u64, 0)));
- return ret.success;
+ if (!bun.canUseCopyFileRangeSyscall()) {
+ return copyFileUsingReadWriteLoop(src, dest, src_fd, dest_fd, size, &wrote);
+ }
+
+ if (size == 0) {
+ // copy until EOF
+ while (true) {
+
+ // Linux Kernel 5.3 or later
+ const written = linux.copy_file_range(src_fd, &off_in_copy, dest_fd, &off_out_copy, std.mem.page_size, 0);
+ if (ret.errnoSysP(written, .copy_file_range, dest)) |err| {
+ return switch (err.getErrno()) {
+ .XDEV, .NOSYS => copyFileUsingReadWriteLoop(src, dest, src_fd, dest_fd, size, &wrote),
+ else => return err,
+ };
+ }
+ // wrote zero bytes means EOF
+ if (written == 0) break;
+ wrote +|= written;
}
- },
- else => {},
- }
+ } else {
+ while (size > 0) {
+ // Linux Kernel 5.3 or later
+ const written = linux.copy_file_range(src_fd, &off_in_copy, dest_fd, &off_out_copy, size, 0);
+ if (ret.errnoSysP(written, .copy_file_range, dest)) |err| {
+ return switch (err.getErrno()) {
+ .XDEV, .NOSYS => copyFileUsingReadWriteLoop(src, dest, src_fd, dest_fd, size, &wrote),
+ else => return err,
+ };
+ }
+ // wrote zero bytes means EOF
+ if (written == 0) break;
+ wrote +|= written;
+ size -|= written;
+ }
+ }
- return Maybe(Return.CopyFile).todo;
+ return ret.success;
+ }
}
+
pub fn exists(this: *NodeFS, args: Arguments.Exists, comptime flavor: Flavor) Maybe(Return.Exists) {
const Ret = Maybe(Return.Exists);
switch (comptime flavor) {
@@ -5018,6 +5329,570 @@ pub const NodeFS = struct {
pub fn createWriteStream(_: *NodeFS, _: Arguments.CreateWriteStream, comptime _: Flavor) Maybe(Return.CreateWriteStream) {
return Maybe(Return.CreateWriteStream).todo;
}
+
+ /// This function is `cpSync`, but only if you pass `{ recursive: ..., force: ..., errorOnExist: ..., mode: ... }'
+ /// The other options like `filter` use a JS fallback, see `src/js/internal/fs/cp.ts`
+ pub fn cp(this: *NodeFS, args: Arguments.Cp, comptime flavor: Flavor) Maybe(Return.Cp) {
+ comptime std.debug.assert(flavor == .sync);
+
+ var src_buf: [bun.MAX_PATH_BYTES]u8 = undefined;
+ var dest_buf: [bun.MAX_PATH_BYTES]u8 = undefined;
+ var src = args.src.sliceZ(&src_buf);
+ var dest = args.dest.sliceZ(&dest_buf);
+
+ return this._cpSync(&src_buf, @intCast(src.len), &dest_buf, @intCast(dest.len), args.flags);
+ }
+
+ fn _cpSync(
+ this: *NodeFS,
+ src_buf: *[bun.MAX_PATH_BYTES]u8,
+ src_dir_len: PathString.PathInt,
+ dest_buf: *[bun.MAX_PATH_BYTES]u8,
+ dest_dir_len: PathString.PathInt,
+ args: Arguments.Cp.Flags,
+ ) Maybe(Return.Cp) {
+ const src = src_buf[0..src_dir_len :0];
+ const dest = dest_buf[0..dest_dir_len :0];
+
+ const stat_ = switch (Syscall.lstat(src)) {
+ .result => |result| result,
+ .err => |err| {
+ @memcpy(this.sync_error_buf[0..src.len], src);
+ return .{ .err = err.withPath(this.sync_error_buf[0..src.len]) };
+ },
+ };
+
+ if (!os.S.ISDIR(stat_.mode)) {
+ const r = this._copySingleFileSync(
+ src,
+ dest,
+ @enumFromInt((if (args.errorOnExist or !args.force) Constants.COPYFILE_EXCL else @as(u8, 0))),
+ stat_,
+ );
+ if (r == .err and r.err.errno == @intFromEnum(os.E.EXIST) and !args.errorOnExist) {
+ return Maybe(Return.Cp).success;
+ }
+ return r;
+ }
+
+ if (!args.recursive) {
+ @memcpy(this.sync_error_buf[0..src.len], src);
+ return .{
+ .err = .{
+ .errno = @intFromEnum(std.os.E.ISDIR),
+ .syscall = .copyfile,
+ .path = this.sync_error_buf[0..src.len],
+ },
+ };
+ }
+
+ if (comptime Environment.isMac) {
+ if (Maybe(Return.Cp).errnoSysP(C.clonefile(src, dest, 0), .clonefile, src)) |err| {
+ switch (err.getErrno()) {
+ .ACCES,
+ .NAMETOOLONG,
+ .ROFS,
+ .NOENT,
+ .PERM,
+ .INVAL,
+ => {
+ @memcpy(this.sync_error_buf[0..src.len], dest);
+ return .{ .err = err.err.withPath(this.sync_error_buf[0..src.len]) };
+ },
+ // Other errors may be due to clonefile() not being supported
+ // We'll fall back to other implementations
+ else => {},
+ }
+ } else {
+ return Maybe(Return.Cp).success;
+ }
+ }
+
+ const flags = os.O.DIRECTORY | os.O.RDONLY;
+ const fd = switch (Syscall.open(src, flags, 0)) {
+ .err => |err| {
+ @memcpy(this.sync_error_buf[0..src.len], src);
+ return .{ .err = err.withPath(this.sync_error_buf[0..src.len]) };
+ },
+ .result => |fd_| fd_,
+ };
+ defer _ = Syscall.close(fd);
+
+ const mkdir_ = this.mkdirRecursive(.{
+ .path = PathLike{ .string = PathString.init(dest) },
+ .recursive = true,
+ }, .sync);
+
+ switch (mkdir_) {
+ .err => |err| return Maybe(Return.Cp){ .err = err },
+ .result => {},
+ }
+
+ var dir = std.fs.Dir{ .fd = fd };
+ var iterator = DirIterator.iterate(dir);
+ var entry = iterator.next();
+ while (switch (entry) {
+ .err => |err| {
+ @memcpy(this.sync_error_buf[0..src.len], src);
+ return .{ .err = err.withPath(this.sync_error_buf[0..src.len]) };
+ },
+ .result => |ent| ent,
+ }) |current| : (entry = iterator.next()) {
+ @memcpy(src_buf[src_dir_len + 1 .. src_dir_len + 1 + current.name.len], current.name.slice());
+ src_buf[src_dir_len] = std.fs.path.sep;
+ src_buf[src_dir_len + 1 + current.name.len] = 0;
+
+ @memcpy(dest_buf[dest_dir_len + 1 .. dest_dir_len + 1 + current.name.len], current.name.slice());
+ dest_buf[dest_dir_len] = std.fs.path.sep;
+ dest_buf[dest_dir_len + 1 + current.name.len] = 0;
+
+ switch (current.kind) {
+ .directory => {
+ const r = this._cpSync(
+ src_buf,
+ src_dir_len + 1 + current.name.len,
+ dest_buf,
+ dest_dir_len + 1 + current.name.len,
+ args,
+ );
+ switch (r) {
+ .err => return r,
+ .result => {},
+ }
+ },
+ else => {
+ const r = this._copySingleFileSync(
+ src_buf[0 .. src_dir_len + 1 + current.name.len :0],
+ dest_buf[0 .. dest_dir_len + 1 + current.name.len :0],
+ @enumFromInt((if (args.errorOnExist or !args.force) Constants.COPYFILE_EXCL else @as(u8, 0))),
+ null,
+ );
+ switch (r) {
+ .err => {
+ if (r.err.errno == @intFromEnum(os.E.EXIST) and !args.errorOnExist) {
+ continue;
+ }
+ return r;
+ },
+ .result => {},
+ }
+ },
+ }
+ }
+ return Maybe(Return.Cp).success;
+ }
+
+ /// This is `copyFile`, but it copies symlinks as-is
+ pub fn _copySingleFileSync(
+ this: *NodeFS,
+ src: [:0]const u8,
+ dest: [:0]const u8,
+ mode: Constants.Copyfile,
+ reuse_stat: ?std.os.Stat,
+ ) Maybe(Return.CopyFile) {
+ const ret = Maybe(Return.CopyFile);
+
+ // TODO: do we need to fchown?
+ if (comptime Environment.isMac) {
+ if (mode.isForceClone()) {
+ // https://www.manpagez.com/man/2/clonefile/
+ return ret.errnoSysP(C.clonefile(src, dest, 0), .clonefile, src) orelse ret.success;
+ } else {
+ const stat_ = reuse_stat orelse switch (Syscall.lstat(src)) {
+ .result => |result| result,
+ .err => |err| {
+ @memcpy(this.sync_error_buf[0..src.len], src);
+ return .{ .err = err.withPath(this.sync_error_buf[0..src.len]) };
+ },
+ };
+
+ if (!os.S.ISREG(stat_.mode)) {
+ if (os.S.ISLNK(stat_.mode)) {
+ var mode_: Mode = C.darwin.COPYFILE_ACL | C.darwin.COPYFILE_DATA | C.darwin.COPYFILE_NOFOLLOW_SRC;
+ if (mode.shouldntOverwrite()) {
+ mode_ |= C.darwin.COPYFILE_EXCL;
+ }
+
+ return ret.errnoSysP(C.copyfile(src, dest, null, mode_), .copyfile, src) orelse ret.success;
+ }
+ @memcpy(this.sync_error_buf[0..src.len], src);
+ return Maybe(Return.CopyFile){ .err = .{
+ .errno = @intFromEnum(C.SystemErrno.ENOTSUP),
+ .path = this.sync_error_buf[0..src.len],
+ } };
+ }
+
+ // 64 KB is about the break-even point for clonefile() to be worth it
+ // at least, on an M1 with an NVME SSD.
+ if (stat_.size > 128 * 1024) {
+ if (!mode.shouldntOverwrite()) {
+ // clonefile() will fail if it already exists
+ _ = Syscall.unlink(dest);
+ }
+
+ if (ret.errnoSysP(C.clonefile(src, dest, 0), .clonefile, src) == null) {
+ _ = C.chmod(dest, stat_.mode);
+ return ret.success;
+ }
+ } else {
+ const src_fd = switch (Syscall.open(src, std.os.O.RDONLY, 0o644)) {
+ .result => |result| result,
+ .err => |err| {
+ @memcpy(this.sync_error_buf[0..src.len], src);
+ return .{ .err = err.withPath(this.sync_error_buf[0..src.len]) };
+ },
+ };
+ defer {
+ _ = Syscall.close(src_fd);
+ }
+
+ var flags: Mode = std.os.O.CREAT | std.os.O.WRONLY;
+ var wrote: usize = 0;
+ if (mode.shouldntOverwrite()) {
+ flags |= std.os.O.EXCL;
+ }
+
+ const dest_fd = dest_fd: {
+ switch (Syscall.open(dest, flags, JSC.Node.default_permission)) {
+ .result => |result| break :dest_fd result,
+ .err => |err| {
+ if (err.getErrno() == .NOENT) {
+ // Create the parent directory if it doesn't exist
+ var len = dest.len;
+ while (len > 0 and dest[len - 1] != std.fs.path.sep) {
+ len -= 1;
+ }
+ const mkdirResult = this.mkdirRecursive(.{
+ .path = PathLike{ .string = PathString.init(dest[0..len]) },
+ .recursive = true,
+ }, .sync);
+ if (mkdirResult == .err) {
+ return Maybe(Return.CopyFile){ .err = mkdirResult.err };
+ }
+
+ switch (Syscall.open(dest, flags, JSC.Node.default_permission)) {
+ .result => |result| break :dest_fd result,
+ .err => {},
+ }
+ }
+
+ @memcpy(this.sync_error_buf[0..dest.len], dest);
+ return Maybe(Return.CopyFile){ .err = err.withPath(this.sync_error_buf[0..dest.len]) };
+ },
+ }
+ };
+ defer {
+ _ = std.c.ftruncate(dest_fd, @as(std.c.off_t, @intCast(@as(u63, @truncate(wrote)))));
+ _ = C.fchmod(dest_fd, stat_.mode);
+ _ = Syscall.close(dest_fd);
+ }
+
+ return copyFileUsingReadWriteLoop(src, dest, src_fd, dest_fd, @intCast(@max(stat_.size, 0)), &wrote);
+ }
+ }
+
+ // we fallback to copyfile() when the file is > 128 KB and clonefile fails
+ // clonefile() isn't supported on all devices
+ // nor is it supported across devices
+ var mode_: Mode = C.darwin.COPYFILE_ACL | C.darwin.COPYFILE_DATA | C.darwin.COPYFILE_NOFOLLOW_SRC;
+ if (mode.shouldntOverwrite()) {
+ mode_ |= C.darwin.COPYFILE_EXCL;
+ }
+
+ return ret.errnoSysP(C.copyfile(src, dest, null, mode_), .copyfile, src) orelse ret.success;
+ }
+
+ if (comptime Environment.isLinux) {
+ // https://manpages.debian.org/testing/manpages-dev/ioctl_ficlone.2.en.html
+ if (mode.isForceClone()) {
+ return Maybe(Return.CopyFile).todo;
+ }
+
+ const src_fd = switch (Syscall.open(src, std.os.O.RDONLY | std.os.O.NOFOLLOW, 0o644)) {
+ .result => |result| result,
+ .err => |err| {
+ if (err.getErrno() == .LOOP) {
+ // ELOOP is returned when you open a symlink with NOFOLLOW.
+ // as in, it does not actually let you open it.
+ return Syscall.symlink(src, dest);
+ }
+
+ return .{ .err = err };
+ },
+ };
+ defer {
+ _ = Syscall.close(src_fd);
+ }
+
+ const stat_: linux.Stat = switch (Syscall.fstat(src_fd)) {
+ .result => |result| result,
+ .err => |err| return Maybe(Return.CopyFile){ .err = err },
+ };
+
+ if (!os.S.ISREG(stat_.mode)) {
+ return Maybe(Return.CopyFile){ .err = .{ .errno = @intFromEnum(C.SystemErrno.ENOTSUP) } };
+ }
+
+ var flags: Mode = std.os.O.CREAT | std.os.O.WRONLY;
+ var wrote: usize = 0;
+ if (mode.shouldntOverwrite()) {
+ flags |= std.os.O.EXCL;
+ }
+
+ const dest_fd = dest_fd: {
+ switch (Syscall.open(dest, flags, JSC.Node.default_permission)) {
+ .result => |result| break :dest_fd result,
+ .err => |err| {
+ if (err.getErrno() == .NOENT) {
+ // Create the parent directory if it doesn't exist
+ var len = dest.len;
+ while (len > 0 and dest[len - 1] != std.fs.path.sep) {
+ len -= 1;
+ }
+ const mkdirResult = this.mkdirRecursive(.{
+ .path = PathLike{ .string = PathString.init(dest[0..len]) },
+ .recursive = true,
+ }, .sync);
+ if (mkdirResult == .err) {
+ return Maybe(Return.CopyFile){ .err = mkdirResult.err };
+ }
+
+ switch (Syscall.open(dest, flags, JSC.Node.default_permission)) {
+ .result => |result| break :dest_fd result,
+ .err => {},
+ }
+ }
+
+ @memcpy(this.sync_error_buf[0..dest.len], dest);
+ return Maybe(Return.CopyFile){ .err = err.withPath(this.sync_error_buf[0..dest.len]) };
+ },
+ }
+ };
+
+ var size: usize = @intCast(@max(stat_.size, 0));
+
+ defer {
+ _ = linux.ftruncate(dest_fd, @as(i64, @intCast(@as(u63, @truncate(wrote)))));
+ _ = linux.fchmod(dest_fd, stat_.mode);
+ _ = Syscall.close(dest_fd);
+ }
+
+ var off_in_copy = @as(i64, @bitCast(@as(u64, 0)));
+ var off_out_copy = @as(i64, @bitCast(@as(u64, 0)));
+
+ if (!bun.canUseCopyFileRangeSyscall()) {
+ return copyFileUsingReadWriteLoop(src, dest, src_fd, dest_fd, size, &wrote);
+ }
+
+ if (size == 0) {
+ // copy until EOF
+ while (true) {
+ // Linux Kernel 5.3 or later
+ const written = linux.copy_file_range(src_fd, &off_in_copy, dest_fd, &off_out_copy, std.mem.page_size, 0);
+ if (ret.errnoSysP(written, .copy_file_range, dest)) |err| {
+ return switch (err.getErrno()) {
+ .XDEV, .NOSYS => copyFileUsingReadWriteLoop(src, dest, src_fd, dest_fd, size, &wrote),
+ else => return err,
+ };
+ }
+ // wrote zero bytes means EOF
+ if (written == 0) break;
+ wrote +|= written;
+ }
+ } else {
+ while (size > 0) {
+ // Linux Kernel 5.3 or later
+ const written = linux.copy_file_range(src_fd, &off_in_copy, dest_fd, &off_out_copy, size, 0);
+ if (ret.errnoSysP(written, .copy_file_range, dest)) |err| {
+ return switch (err.getErrno()) {
+ .XDEV, .NOSYS => copyFileUsingReadWriteLoop(src, dest, src_fd, dest_fd, size, &wrote),
+ else => return err,
+ };
+ }
+ // wrote zero bytes means EOF
+ if (written == 0) break;
+ wrote +|= written;
+ size -|= written;
+ }
+ }
+
+ return ret.success;
+ }
+
+ return ret.todo;
+ }
+
+ /// Directory scanning + clonefile will block this thread, then each individual file copy (what the sync version
+ /// calls "_copySingleFileSync") will be dispatched as a separate task.
+ pub fn cpAsync(this: *NodeFS, task: *AsyncCpTask) void {
+ const args = task.args;
+ var src_buf: [bun.MAX_PATH_BYTES]u8 = undefined;
+ var dest_buf: [bun.MAX_PATH_BYTES]u8 = undefined;
+ var src = args.src.sliceZ(&src_buf);
+ var dest = args.dest.sliceZ(&dest_buf);
+
+ const stat_ = switch (Syscall.lstat(src)) {
+ .result => |result| result,
+ .err => |err| {
+ @memcpy(this.sync_error_buf[0..src.len], src);
+ task.finishConcurrently(.{ .err = err.withPath(this.sync_error_buf[0..src.len]) });
+ return;
+ },
+ };
+
+ if (!os.S.ISDIR(stat_.mode)) {
+ // This is the only file, there is no point in dispatching subtasks
+ const r = this._copySingleFileSync(
+ src,
+ dest,
+ @enumFromInt((if (args.flags.errorOnExist or !args.flags.force) Constants.COPYFILE_EXCL else @as(u8, 0))),
+ stat_,
+ );
+ if (r == .err and r.err.errno == @intFromEnum(os.E.EXIST) and !args.flags.errorOnExist) {
+ task.finishConcurrently(Maybe(Return.Cp).success);
+ return;
+ }
+ task.finishConcurrently(r);
+ return;
+ }
+
+ if (!args.flags.recursive) {
+ @memcpy(this.sync_error_buf[0..src.len], src);
+ task.finishConcurrently(.{ .err = .{
+ .errno = @intFromEnum(std.os.E.ISDIR),
+ .syscall = .copyfile,
+ .path = this.sync_error_buf[0..src.len],
+ } });
+ return;
+ }
+
+ const success = this._cpAsyncDirectory(args.flags, task, &src_buf, @intCast(src.len), &dest_buf, @intCast(dest.len));
+ const old_count = task.subtask_count.fetchSub(1, .Monotonic);
+ if (success and old_count == 1) {
+ task.finishConcurrently(Maybe(Return.Cp).success);
+ }
+ }
+
+ // returns boolean `should_continue`
+ fn _cpAsyncDirectory(
+ this: *NodeFS,
+ args: Arguments.Cp.Flags,
+ task: *AsyncCpTask,
+ src_buf: *[bun.MAX_PATH_BYTES]u8,
+ src_dir_len: PathString.PathInt,
+ dest_buf: *[bun.MAX_PATH_BYTES]u8,
+ dest_dir_len: PathString.PathInt,
+ ) bool {
+ const src = src_buf[0..src_dir_len :0];
+ const dest = dest_buf[0..dest_dir_len :0];
+
+ if (comptime Environment.isMac) {
+ if (Maybe(Return.Cp).errnoSysP(C.clonefile(src, dest, 0), .clonefile, src)) |err| {
+ switch (err.getErrno()) {
+ .ACCES,
+ .NAMETOOLONG,
+ .ROFS,
+ .NOENT,
+ .PERM,
+ .INVAL,
+ => {
+ @memcpy(this.sync_error_buf[0..src.len], dest);
+ task.finishConcurrently(.{ .err = err.err.withPath(this.sync_error_buf[0..src.len]) });
+ return false;
+ },
+ // Other errors may be due to clonefile() not being supported
+ // We'll fall back to other implementations
+ else => {},
+ }
+ } else {
+ return true;
+ }
+ }
+
+ const open_flags = os.O.DIRECTORY | os.O.RDONLY;
+ const fd = switch (Syscall.open(src, open_flags, 0)) {
+ .err => |err| {
+ @memcpy(this.sync_error_buf[0..src.len], src);
+ task.finishConcurrently(.{ .err = err.withPath(this.sync_error_buf[0..src.len]) });
+ return false;
+ },
+ .result => |fd_| fd_,
+ };
+ defer _ = Syscall.close(fd);
+
+ const mkdir_ = this.mkdirRecursive(.{
+ .path = PathLike{ .string = PathString.init(dest) },
+ .recursive = true,
+ }, .sync);
+ switch (mkdir_) {
+ .err => |err| {
+ task.finishConcurrently(.{ .err = err });
+ return false;
+ },
+ .result => {},
+ }
+
+ var dir = std.fs.Dir{ .fd = fd };
+ var iterator = DirIterator.iterate(dir);
+ var entry = iterator.next();
+ while (switch (entry) {
+ .err => |err| {
+ @memcpy(this.sync_error_buf[0..src.len], src);
+ task.finishConcurrently(.{ .err = err.withPath(this.sync_error_buf[0..src.len]) });
+ return false;
+ },
+ .result => |ent| ent,
+ }) |current| : (entry = iterator.next()) {
+ switch (current.kind) {
+ .directory => {
+ @memcpy(src_buf[src_dir_len + 1 .. src_dir_len + 1 + current.name.len], current.name.slice());
+ src_buf[src_dir_len] = std.fs.path.sep;
+ src_buf[src_dir_len + 1 + current.name.len] = 0;
+
+ @memcpy(dest_buf[dest_dir_len + 1 .. dest_dir_len + 1 + current.name.len], current.name.slice());
+ dest_buf[dest_dir_len] = std.fs.path.sep;
+ dest_buf[dest_dir_len + 1 + current.name.len] = 0;
+
+ const should_continue = this._cpAsyncDirectory(
+ args,
+ task,
+ src_buf,
+ src_dir_len + 1 + current.name.len,
+ dest_buf,
+ dest_dir_len + 1 + current.name.len,
+ );
+ if (!should_continue) return false;
+ },
+ else => {
+ _ = task.subtask_count.fetchAdd(1, .Monotonic);
+
+ // Allocate a path buffer for the path data
+ var path_buf = bun.default_allocator.alloc(
+ u8,
+ src_dir_len + 1 + current.name.len + 1 + dest_dir_len + 1 + current.name.len + 1,
+ ) catch @panic("Out of memory");
+
+ @memcpy(path_buf[0..src_dir_len], src_buf[0..src_dir_len]);
+ path_buf[src_dir_len] = std.fs.path.sep;
+ @memcpy(path_buf[src_dir_len + 1 .. src_dir_len + 1 + current.name.len], current.name.slice());
+ path_buf[src_dir_len + 1 + current.name.len] = 0;
+
+ @memcpy(path_buf[src_dir_len + 1 + current.name.len + 1 .. src_dir_len + 1 + current.name.len + 1 + dest_dir_len], dest_buf[0..dest_dir_len]);
+ path_buf[src_dir_len + 1 + current.name.len + 1 + dest_dir_len] = std.fs.path.sep;
+ @memcpy(path_buf[src_dir_len + 1 + current.name.len + 1 + dest_dir_len + 1 .. src_dir_len + 1 + current.name.len + 1 + dest_dir_len + 1 + current.name.len], current.name.slice());
+ path_buf[src_dir_len + 1 + current.name.len + 1 + dest_dir_len + 1 + current.name.len] = 0;
+
+ AsyncCpSingleFileTask.create(
+ task,
+ path_buf[0 .. src_dir_len + 1 + current.name.len :0],
+ path_buf[src_dir_len + 1 + current.name.len + 1 .. src_dir_len + 1 + current.name.len + 1 + dest_dir_len + 1 + current.name.len :0],
+ );
+ },
+ }
+ }
+
+ return true;
+ }
};
pub export fn Bun__mkdirp(globalThis: *JSC.JSGlobalObject, path: [*:0]const u8) bool {
diff --git a/src/bun.js/node/node_fs_binding.zig b/src/bun.js/node/node_fs_binding.zig
index dfc47b2ce..16128be44 100644
--- a/src/bun.js/node/node_fs_binding.zig
+++ b/src/bun.js/node/node_fs_binding.zig
@@ -108,9 +108,12 @@ fn call(comptime FunctionEnum: NodeFSFunctionEnum) NodeFSFunction {
globalObject: *JSC.JSGlobalObject,
callframe: *JSC.CallFrame,
) callconv(.C) JSC.JSValue {
- if (comptime FunctionEnum != .readdir and FunctionEnum != .lstat and FunctionEnum != .stat and FunctionEnum != .readFile and FunctionEnum != .realpath) {
- globalObject.throw("Not implemented yet", .{});
- return .zero;
+ switch (comptime FunctionEnum) {
+ .readdir, .lstat, .stat, .readFile, .realpath, .copyFile, .cp => {},
+ else => {
+ globalObject.throw("Not implemented yet", .{});
+ return .zero;
+ },
}
var arguments = callframe.arguments(8);
@@ -155,6 +158,14 @@ fn call(comptime FunctionEnum: NodeFSFunctionEnum) NodeFSFunction {
return JSC.Node.AsyncStatTask.create(globalObject, args, slice.vm, FunctionEnum == .lstat, slice.arena);
}
+ if (comptime FunctionEnum == .copyFile) {
+ return JSC.Node.AsyncCopyFileTask.create(globalObject, args, slice.vm, slice.arena);
+ }
+
+ if (comptime FunctionEnum == .cp) {
+ return JSC.Node.AsyncCpTask.create(globalObject, args, slice.vm, slice.arena);
+ }
+
// defer {
// for (arguments.len) |arg| {
// JSC.C.JSValueUnprotect(ctx, arg);
@@ -201,6 +212,7 @@ pub const NodeJSFS = struct {
pub const appendFile = call(.appendFile);
pub const close = call(.close);
pub const copyFile = call(.copyFile);
+ pub const cp = call(.cp);
pub const exists = call(.exists);
pub const chown = call(.chown);
pub const chmod = call(.chmod);
@@ -236,6 +248,7 @@ pub const NodeJSFS = struct {
pub const accessSync = callSync(.access);
pub const appendFileSync = callSync(.appendFile);
pub const closeSync = callSync(.close);
+ pub const cpSync = callSync(.cp);
pub const copyFileSync = callSync(.copyFile);
pub const existsSync = callSync(.exists);
pub const chownSync = callSync(.chown);