Add create/createNew options to Deno.copyFile #4584
Closed
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
We add
create
andcreateNew
options tocopyFile
, paralleling those foropen
andwriteFile
. See #4569, which discusses how this relates to some concurrent PRs, and also compares to peer languages. As mentioned there,Deno.copyFile
withcreateNew: true
corresponds to Node'sfs.copyFile
with theCOPYFILE_EXCL
flag, and to the POSIX shell commandcp -Tn
.This is part of larger series of filesystem commits (#4017).
When possible, we use the fastest implementation provided by Rust (
std::fs::copy
;create
must be true andcreateNew
false). In other cases, for the operations to still be atomic, we need to open two files and copy the data usingstd::io::copy
. That may be slower because it requires copying data out of kernel space.Note for the future: if we ever shift to a
tokio::fs
version ofop_copy_file
(see refactor: ops/fs.rs should use tokio::fs where applicable #4188), I discovered that Tokio's implementation of this always uses the slower path, viatokio::io::copy
. It also wasn't copying permissions correctly. (See tokio::fs::copy sometimes not copying permissions? tokio-rs/tokio#2341.)(PRs I plan to propose later make use of some Unix-specific functionality when available, which means bypassing Tokio here anyway.)
As with
open
in Cleanup Deno.open and Deno.writeFile #4580, this PR makes a special effort to ensure that the errors triggered bycreateNew: true
are always AlreadyExists. We also ensure that the errors triggered bycreate: false
are always NotFound. In other cases, we let the different platforms generate their different errors. (For example, on Unix attempting to copy a file onto a directory gives a "Is a directory" error, but on Windows it's a PermissionDenied.)This PR also makes an effort to ensure that when the source path is a directory, we get a predictable error, regardless of the presence of
create
orcreateNew
options governing the destination path. That is, the check on the source takes priority.(I didn't set out aiming for any specific pattern of errors. Just hoped to find minimal interventions that did the most towards cleaning up the messy complexity we were getting of which errors were reported how, before this PR, and then the different messy complexity we were getting after introducing the other changes in this PR. I think I found a good balance. It involves changing one Unix error and one Windows error. See comments in op_copy_file.)
As with
writeFile
in Cleanup Deno.open and Deno.writeFile #4580,copyFile
has a very simple version of the internalcheckOpenOptions
function, which one may be tempted to inline. I left it factored out because later PRs which we might consider expand would expand this function.We refactor some
copyFile
tests to avoid repetition (assertFile
function).We add tests of the new
create
andcreateNew
functionality. We also add tests documenting the different errors that Windows and Unix give when trying to copy onto directories. (It would be good to know if/when we ever build on a system where these are different.) And verifying that the errors are always AlreadyExists if triggered bycreateNew
being true.We haven't yet implemented creating symlinks on Windows. On Unix, we verify that
copyFile
gives an error when writing to a path that contains any valid symlink whencreateNew
istrue
, and that the error is alwaysAlreadyExists
. Also that it gives a (different) error whencreateNew
is false, but writing to a path that contains a symlink to a directory. If the options are{ create: false, createNew: false }
, and the target is a dangling symlink, the error is `NotFound.The underlying Rust machinery we rely on doesn't give an error when
create
orcreateNew
aretrue
, but writing to a path that contains a dangling symlink. Instead it creates a new file at the target destination, and copies the file there. (It "copies through" the dangling symlink.) Python'sshutil.copy
and Node'sfs.copyFileSync
also do this. The shellcp
operation on the other hand, won't perform the copy if the destination is a dangling symlink. We'd have to make special efforts (with runtime cost) to match the shell, and I haven't done that here.Some of the
copyFile
tests in our codebase rely on the older behavior ofwriteFile
wrtmode
, and need to be updated after Cleanup Deno.open and Deno.writeFile #4580 lands. Whichever of this PR or Cleanup Deno.open and Deno.writeFile #4580 is merged first, I can push a commit to the other PR that updates those copyFile tests.