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

stdlib/os: handle symlinks in copy/move functions #16709

Merged
merged 3 commits into from
Feb 4, 2021

Conversation

rominf
Copy link
Contributor

@rominf rominf commented Jan 13, 2021

  • Added optional options argument to copyFile, copyFileToDir, and
    copyFileWithPermissions. By default, symlinks are followed (copy files
    symlinks point to). On Windows, options == {cfSymlinkAsIs} do not affect.
  • copyDir and copyDirWithPermissions copy symlinks as they are (instead of
    skipping them as it was before). On Windows, symlinks are followed.
  • moveFile and moveDir move symlinks as they are (instead of skipping them
    sometimes as it was before).
  • Added optional followSymlinks argument to setFilePermissions.

See also: nim-lang/RFCs#319

@rominf rominf marked this pull request as draft January 13, 2021 18:25
@rominf rominf force-pushed the rominf-os-copy-add-follow-symlinks-arg branch 2 times, most recently from b11aedd to 1b2eb5e Compare January 13, 2021 18:31
lib/pure/os.nim Outdated Show resolved Hide resolved
@rominf
Copy link
Contributor Author

rominf commented Jan 13, 2021

Well, we get:

/home/vsts/work/1/s/lib/pure/os.nim(1653, 6) Error: 'copyFile' is not GC-safe as it calls 'expandSymlink'

What's the way to fix this? I'm not qualified enough yet to fix this...

@rominf rominf force-pushed the rominf-os-copy-add-follow-symlinks-arg branch from 1b2eb5e to 92621c8 Compare January 13, 2021 18:42
tests/stdlib/tos.nim Outdated Show resolved Hide resolved
@rominf rominf force-pushed the rominf-os-copy-add-follow-symlinks-arg branch from 92621c8 to 2199cf6 Compare January 13, 2021 18:48
tests/stdlib/tos.nim Outdated Show resolved Hide resolved
@rominf rominf force-pushed the rominf-os-copy-add-follow-symlinks-arg branch from 2199cf6 to 2476982 Compare January 13, 2021 18:56
lib/pure/os.nim Outdated Show resolved Hide resolved
lib/pure/os.nim Outdated Show resolved Hide resolved
@timotheecour
Copy link
Member

Error: 'copyFile' is not GC-safe as it calls 'expandSymlink'
What's the way to fix this? I'm not qualified enough yet to fix this...

follow the error messages in logs; it shows it fails with bin/nim c -o:bin/nimsuggest -d:release -d:danger nimsuggest/nimsuggest.nim;

then run that locally, it fails; then see why it fails: it's because nimsuggest/nimsuggest.nim.cfg contains --threads:on

then run nim r --lib:lib tests/stdlib/tos.nim , it now also fails

the fix: add gcsafe annotations for decl+impl for createSymlink, expandSymlink

  • PR welcome for adding a single test that simply imports a bunch of stdlib files and compiles with --threads:on, to prevent future regressions along those lines that wouldn't be caught by simply compiling nimsuggest

@juancarlospaco
Copy link
Collaborator

Check that those still work on NimScript because they currently do.

I really appreciate the effort, if you are going to improve it remember that Nim can not differentiate a file from a folder. 🤷

@rominf
Copy link
Contributor Author

rominf commented Jan 15, 2021

How other languages handle symlinks in copyDir analogs

Python

shutil.copytree(src, dst, symlinks=False, ignore=None, copy_function=copy2, ignore_dangling_symlinks=False, dirs_exist_ok=False)
...
If symlinks is true, symbolic links in the source tree are represented as symbolic links in the new tree and the metadata of the original links will be copied as far as the platform allows; if false or omitted, the contents and metadata of the linked files are copied to the new tree.

Docs: https://docs.python.org/3/library/shutil.html#shutil.copytree

Rust

There is no copyDir in the standard library.

SO question: https://stackoverflow.com/questions/26958489/how-to-copy-a-folder-recursively-in-rust

C++

options controlling the effects of copy() on symbolic links
--
none | Follow symlinks (default behavior)

Docs: https://en.cppreference.com/w/cpp/filesystem/copy_options

Go

There is no copyDir in the standard library.

A most popular library for Go copies symlinks instead of following them.

Sources: https://github.com/otiai10/copy/blob/1dfb4e4bf632edd08bc1a853160ad03ff18e68b5/options.go#L66

Julia

cp(src::AbstractString, dst::AbstractString; force::Bool=false, follow_symlinks::Bool=false)
...
If follow_symlinks=false, and src is a symbolic link, dst will be created as a symbolic link.

Docs: https://docs.julialang.org/en/v1/base/file/#Base.Filesystem.cp

Conclusions

Some languages don't have copyDir at all; some follow symlinks, some do not. 🤷‍♂️

My thoughts

I'm tempted to make it the same way as C++ does: follow symlinks by default and to pass the options (ORed enum CopyOptions which has values none, copy_symlinks, skip_symlinks).

Reasoning:

  1. I think we need to keep backward compatibility, so copyFile should follow symlinks.
  2. I think we need to be consistent. So, since copyDir doesn't copy symlinks at all, which is a bug, in my opinion, we can break backward compatibility and (to be consistent) follow symlinks.
  3. In the future, there could be a need for more options for copying than "follow symlinks." Also, the large variety of implementations show that it's not a trivial function. So I would prefer to be on the safe side, adding the options ORed enum which can be extended in the future.

So, what do you think?

@timotheecour
Copy link
Member

timotheecour commented Jan 15, 2021

I think we need to keep backward compatibility, so copyFile should follow symlinks.

agreed here, and also it's a reasonable default for copyFile

In the future, there could be a need for more options for copying than "follow symlinks."

I agree. But I'd prefer using a

type CopyOptions* = object
  followSymlinks*: bool
proc initCopyOptions(): CopyOptions = ...

for same rationale as in jsonutils #15133 (comment)
instead of a raw bool or instead of a set[SomeOptions]. An object is more flexible than a set of enum (eg can add in future non-bool options; there are other advantages); the main advantage of set, efficiency, is not a concern here given the cost of copy.

eg:

let opt = initCopyOptions()
opt.follow
copyDir(src, dst, opt)

I think we need to be consistent. So, since copyDir doesn't copy symlinks at all, which is a bug, in my opinion, we can break backward compatibility and (to be consistent) follow symlinks.

that's the only part i disagree with, and I disagree strongly. following symlinks should be opt in when it comes to copyDir which is recursive. Imagine you call:

mkdir foo
touch foo/test.txt
ln -s ~ foo/myhome

copyDir "foo", "foo2"
before PR: foo/myhome symlink is skipped
after PR with followSymlinks defaulting to true for coyDir: user's home directory ~ is (recursively) copied, a large regression
after PR with followSymlinks defaulting to false for coyDir: the symlink is copied as is

and then there are other problems too, eg handling internal symlinks etc (these are in fact common), or even cyclic links.

@rominf
Copy link
Contributor Author

rominf commented Jan 16, 2021

@juancarlospaco, you wrote:

Check that those still work on NimScript because they currently do.

I don't think so, I've just tried it:

$ cat test_nimscript.nims
#!/usr/bin/env nim
mode = ScriptMode.Silent

import os

copyFile("./test_nimscript.nims", "./test_nimscript_copy.nims")
$ ./test_nimscript.nims
Hint: used config file '/home/rominf/dev/nim/config/nim.cfg' [Conf]
Hint: used config file '/home/rominf/dev/nim/config/config.nims' [Conf]
/tmp/test_nimscript.nims(6, 1) Error: this proc is not available on the NimScript/js target; usage of 'copyFile' is an {.error.} defined at /home/rominf/dev/nim/lib/pure/os.nim(1650, 1)

@timotheecour
Copy link
Member

timotheecour commented Jan 16, 2021

@rominf for nimscript, try instead cpFile, cpDir (https://nim-lang.github.io/Nim/nimscript.html)

no need to add API's to std/nimscript though (the preferred way is for nims to import os intead of using its own aliases in nimscript), but yes, we should ensure cpFile, cpDir still work

just extend tests/test_nimscript.nims with the minimum needed

@rominf rominf force-pushed the rominf-os-copy-add-follow-symlinks-arg branch 2 times, most recently from efc9a3c to c665b26 Compare January 16, 2021 18:15
@rominf rominf marked this pull request as ready for review January 16, 2021 18:41
lib/pure/os.nim Outdated Show resolved Hide resolved
@rominf rominf force-pushed the rominf-os-copy-add-follow-symlinks-arg branch from c665b26 to 14574c7 Compare January 16, 2021 18:59
lib/pure/os.nim Outdated Show resolved Hide resolved
@rominf rominf force-pushed the rominf-os-copy-add-follow-symlinks-arg branch 5 times, most recently from 1e3441d to bfa077a Compare January 16, 2021 19:31
@rominf rominf marked this pull request as ready for review January 31, 2021 08:49
@Araq
Copy link
Member

Araq commented Feb 2, 2021

IMO most sensible behavior is as follows: for copyDir on windows, try to copy symlinks, and if it fails, write a sensible error message on stderr (with runtime context), but don't fail. Future work can add options for copyDir to control this.

What?! No, we don't produce output on stderr in undocumented ways.

@rominf
Copy link
Contributor Author

rominf commented Feb 2, 2021

@Araq Ok, I understand your point. How do you propose to solve this?

@Araq
Copy link
Member

Araq commented Feb 2, 2021

Ok, I understand your point. How do you propose to solve this?

Pretend for the time being that Windows has no symlinks and fix copyDir for Posix only.

Roman Inflianskas and others added 2 commits February 4, 2021 14:48
- Added optional `options` argument to `copyFile`, `copyFileToDir`, and
  `copyFileWithPermissions`. By default, symlinks are followed (copy files
  symlinks point to).
- `copyDir` and `copyDirWithPermissions` copy symlinks as symlinks (instead of
  skipping them as it was before).
- `moveFile` and `moveDir` move symlinks as symlinks (instead of skipping them
  sometimes as it was before).
- Added optional `followSymlinks` argument to `setFilePermissions`.

See also: nim-lang/RFCs#319

Co-authored-by: Timothee Cour <[email protected]>
Co-authored-by: Timothee Cour <[email protected]>
rominf pushed a commit to rominf/Nim that referenced this pull request Feb 4, 2021
@rominf rominf force-pushed the rominf-os-copy-add-follow-symlinks-arg branch from fe11588 to c46126b Compare February 4, 2021 13:17
rominf pushed a commit to rominf/Nim that referenced this pull request Feb 4, 2021
@rominf rominf force-pushed the rominf-os-copy-add-follow-symlinks-arg branch from c46126b to a9a6847 Compare February 4, 2021 13:20
rominf pushed a commit to rominf/Nim that referenced this pull request Feb 4, 2021
@rominf rominf force-pushed the rominf-os-copy-add-follow-symlinks-arg branch from a9a6847 to c704902 Compare February 4, 2021 13:25
rominf pushed a commit to rominf/Nim that referenced this pull request Feb 4, 2021
@rominf rominf force-pushed the rominf-os-copy-add-follow-symlinks-arg branch from c704902 to 90db1cd Compare February 4, 2021 13:27
@rominf rominf force-pushed the rominf-os-copy-add-follow-symlinks-arg branch from 90db1cd to c1301f7 Compare February 4, 2021 13:55
@Araq
Copy link
Member

Araq commented Feb 4, 2021

Merging this now, will clean it up a bit later.

@Araq Araq merged commit e9b360c into nim-lang:devel Feb 4, 2021
@rominf rominf deleted the rominf-os-copy-add-follow-symlinks-arg branch February 4, 2021 18:02
- On non-Windows OSes, `copyDir` and `copyDirWithPermissions` copy symlinks as
symlinks (instead of skipping them as it was before); on Windows symlinks are
skipped.
- On non-Windows OSes, `moveFile` and `moveDir` move symlinks as symlinks
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@rominf how about on windows, what is the behavior now? (even if it hasn't changed); when reading the changelog, one might wonder


proc moveDir*(source, dest: string) {.tags: [ReadIOEffect, WriteIOEffect], noWeirdTarget.} =
## Moves a directory from `source` to `dest`.
##
## Symlinks are not followed: if `source` contains symlinks, they themself are
## moved, not their target.
Copy link
Member

@timotheecour timotheecour Feb 4, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what's the behavior for windows when source is a symlink? noop? remove source? something else?

except:
if not ignorePermissionErrors:
raise

proc copyDirWithPermissions*(source, dest: string,
ignorePermissionErrors = true) {.rtl, extern: "nos$1",
tags: [WriteIOEffect, ReadIOEffect], benign, noWeirdTarget.} =
ignorePermissionErrors = true)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

in future work, copyDirWithPermissions and copyDir should be refactored with a common implementation copyDirImpl to avoid the duplication (but see also timotheecour#570 which suggests using a single copyDir with options, which would make copyDirImpl un-necessary)

## Copies a directory from `source` to `dest`.
##
## On non-Windows OSes, symlinks are copied as symlinks. On Windows, symlinks
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • pre-existing but what's the behavior when dest already exists? (on each OS)
  • also, we should document that dest.parentDir doesn't need to exist

mkDir(subDir)
writeFile(fpath, "some text")
cpFile(fpath, fpath2)
doAssert fileExists(fpath2)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should add:
doAssert fpath2.readFile == "some text"

Comment on lines +194 to +212
when symlinksAreHandled:
doAssertRaises(OSError):
copyFileWithPermissions(brokenSymlink, brokenSymlinkCopy)
doAssertRaises(OSError):
copyFileWithPermissions(brokenSymlink, brokenSymlinkCopy,
options = {cfSymlinkFollow})
copyFileWithPermissions(brokenSymlink, brokenSymlinkCopy,
options = {cfSymlinkIgnore})
doAssert not fileExists(brokenSymlinkCopy)
copyFileWithPermissions(brokenSymlink, brokenSymlinkCopy,
options = {cfSymlinkAsIs})
when symlinksAreHandled:
doAssert expandSymlink(brokenSymlinkCopy) == brokenSymlinkSrc
removeFile(brokenSymlinkCopy)
else:
doAssert not fileExists(brokenSymlinkCopy)
doAssertRaises(AssertionDefect):
copyFileWithPermissions(brokenSymlink, brokenSymlinkCopy,
options = {cfSymlinkAsIs, cfSymlinkFollow})
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use a template to avoid this entire block being duplicated:
eg:

template test(algo) =
  ...
test copyFile
test copyFileWithPermissions

copyFileWithPermissions(brokenSymlink, brokenSymlinkCopy,
options = {cfSymlinkAsIs, cfSymlinkFollow})

# Test copyFileToDir
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

likewise, there should be a way to avoid duplication even for copyFileToDir block


# Test moveFile
moveFile(brokenSymlink, brokenSymlinkCopy)
when not defined(windows):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why not: when symlinksAreHandled: ?


# Test moveDir
moveDir(subDir, subDir2)
when not defined(windows):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ditto, when symlinksAreHandled:?

@timotheecour
Copy link
Member

@rominf excellent change, and also the added tests are really good. I added some post-review comments for future PRs;

IMO we should still support symlinks on windows, but deferring this to a future PR makes sense as this PR was already complex enough

ardek66 pushed a commit to ardek66/Nim that referenced this pull request Mar 26, 2021
* stdlib/os: handle symlinks in copy/move functions

- Added optional `options` argument to `copyFile`, `copyFileToDir`, and
  `copyFileWithPermissions`. By default, symlinks are followed (copy files
  symlinks point to).
- `copyDir` and `copyDirWithPermissions` copy symlinks as symlinks (instead of
  skipping them as it was before).
- `moveFile` and `moveDir` move symlinks as symlinks (instead of skipping them
  sometimes as it was before).
- Added optional `followSymlinks` argument to `setFilePermissions`.

See also: nim-lang/RFCs#319

Co-authored-by: Timothee Cour <[email protected]>

* Address comments in nim-lang#16709

Co-authored-by: Timothee Cour <[email protected]>

* Address comments in nim-lang#16709 (second iteration)

Skip symlinks on Windows.

Co-authored-by: Timothee Cour <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants