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

* 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: https://github.com/nim-lang/RFCs/issues/319

Co-authored-by: Timothee Cour <timothee.cour2@gmail.com>

* Address comments in #16709

Co-authored-by: Timothee Cour <timothee.cour2@gmail.com>

* Address comments in #16709 (second iteration)

Skip symlinks on Windows.

Co-authored-by: Timothee Cour <timothee.cour2@gmail.com>
This commit is contained in:
Roman Inflianskas 2021-02-04 09:57:41 -08:00 • committed by GitHub
commit e9b360c5df
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 321 additions and 106 deletions

View file

@ -112,6 +112,18 @@ with other backends. see #9125. Use `-d:nimLegacyJsRound` for previous behavior.
- Removed the optional `longestMatch` parameter of the `critbits._WithPrefix` iterators (it never worked reliably) - Removed the optional `longestMatch` parameter of the `critbits._WithPrefix` iterators (it never worked reliably)
- Added optional `options` argument to `copyFile`, `copyFileToDir`, and
`copyFileWithPermissions`. By default, on non-Windows OSes, symlinks are
followed (copy files symlinks point to); on Windows, `options` argument is
ignored and symlinks are skipped.
- 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
(instead of skipping them sometimes as it was before).
- Added optional `followSymlinks` argument to `setFilePermissions`.
## Language changes ## Language changes
- `nimscript` now handles `except Exception as e`. - `nimscript` now handles `except Exception as e`.

View file

@ -586,6 +586,8 @@ proc fstatvfs*(a1: cint, a2: var Statvfs): cint {.
importc, header: "<sys/statvfs.h>".} importc, header: "<sys/statvfs.h>".}
proc chmod*(a1: cstring, a2: Mode): cint {.importc, header: "<sys/stat.h>", sideEffect.} proc chmod*(a1: cstring, a2: Mode): cint {.importc, header: "<sys/stat.h>", sideEffect.}
when defined(osx) or defined(freebsd):
proc lchmod*(a1: cstring, a2: Mode): cint {.importc, header: "<sys/stat.h>", sideEffect.}
proc fchmod*(a1: cint, a2: Mode): cint {.importc, header: "<sys/stat.h>", sideEffect.} proc fchmod*(a1: cint, a2: Mode): cint {.importc, header: "<sys/stat.h>", sideEffect.}
proc fstat*(a1: cint, a2: var Stat): cint {.importc, header: "<sys/stat.h>", sideEffect.} proc fstat*(a1: cint, a2: var Stat): cint {.importc, header: "<sys/stat.h>", sideEffect.}
proc lstat*(a1: cstring, a2: var Stat): cint {.importc, header: "<sys/stat.h>", sideEffect.} proc lstat*(a1: cstring, a2: var Stat): cint {.importc, header: "<sys/stat.h>", sideEffect.}

View file

@ -1592,10 +1592,18 @@ proc getFilePermissions*(filename: string): set[FilePermission] {.
else: else:
result = {fpUserExec..fpOthersRead} result = {fpUserExec..fpOthersRead}
proc setFilePermissions*(filename: string, permissions: set[FilePermission]) {. proc setFilePermissions*(filename: string, permissions: set[FilePermission],
rtl, extern: "nos$1", tags: [WriteDirEffect], noWeirdTarget.} = followSymlinks = true)
{.rtl, extern: "nos$1", tags: [ReadDirEffect, WriteDirEffect],
noWeirdTarget.} =
## Sets the file permissions for `filename`. ## Sets the file permissions for `filename`.
## ##
## If `followSymlinks` set to true (default) and ``filename`` points to a
## symlink, permissions are set to the file symlink points to.
## `followSymlinks` set to false is a noop on Windows and some POSIX
## systems (including Linux) on which `lchmod` is either unavailable or always
## fails, given that symlinks permissions there are not observed.
##
## `OSError` is raised in case of an error. ## `OSError` is raised in case of an error.
## On Windows, only the ``readonly`` flag is changed, depending on ## On Windows, only the ``readonly`` flag is changed, depending on
## ``fpUserWrite`` permission. ## ``fpUserWrite`` permission.
@ -1617,7 +1625,13 @@ proc setFilePermissions*(filename: string, permissions: set[FilePermission]) {.
if fpOthersWrite in permissions: p = p or S_IWOTH.Mode if fpOthersWrite in permissions: p = p or S_IWOTH.Mode
if fpOthersExec in permissions: p = p or S_IXOTH.Mode if fpOthersExec in permissions: p = p or S_IXOTH.Mode
if chmod(filename, cast[Mode](p)) != 0: raiseOSError(osLastError(), $(filename, permissions)) if not followSymlinks and filename.symlinkExists:
when declared(lchmod):
if lchmod(filename, cast[Mode](p)) != 0:
raiseOSError(osLastError(), $(filename, permissions))
else:
if chmod(filename, cast[Mode](p)) != 0:
raiseOSError(osLastError(), $(filename, permissions))
else: else:
when useWinUnicode: when useWinUnicode:
wrapUnary(res, getFileAttributesW, filename) wrapUnary(res, getFileAttributesW, filename)
@ -1634,6 +1648,53 @@ proc setFilePermissions*(filename: string, permissions: set[FilePermission]) {.
var res2 = setFileAttributesA(filename, res) var res2 = setFileAttributesA(filename, res)
if res2 == - 1'i32: raiseOSError(osLastError(), $(filename, permissions)) if res2 == - 1'i32: raiseOSError(osLastError(), $(filename, permissions))
proc createSymlink*(src, dest: string) {.noWeirdTarget.} =
## Create a symbolic link at `dest` which points to the item specified
## by `src`. On most operating systems, will fail if a link already exists.
##
## **Warning**:
## Some OS's (such as Microsoft Windows) restrict the creation
## of symlinks to root users (administrators) or users with developper mode enabled.
##
## See also:
## * `createHardlink proc <#createHardlink,string,string>`_
## * `expandSymlink proc <#expandSymlink,string>`_
when defined(Windows):
const SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE = 2
# allows anyone with developer mode on to create a link
let flag = dirExists(src).int32 or SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE
when useWinUnicode:
var wSrc = newWideCString(src)
var wDst = newWideCString(dest)
if createSymbolicLinkW(wDst, wSrc, flag) == 0 or getLastError() != 0:
raiseOSError(osLastError(), $(src, dest))
else:
if createSymbolicLinkA(dest, src, flag) == 0 or getLastError() != 0:
raiseOSError(osLastError(), $(src, dest))
else:
if symlink(src, dest) != 0:
raiseOSError(osLastError(), $(src, dest))
proc expandSymlink*(symlinkPath: string): string {.noWeirdTarget.} =
## Returns a string representing the path to which the symbolic link points.
##
## On Windows this is a noop, `symlinkPath` is simply returned.
##
## See also:
## * `createSymlink proc <#createSymlink,string,string>`_
when defined(windows):
result = symlinkPath
else:
result = newString(maxSymlinkLen)
var len = readlink(symlinkPath, result, maxSymlinkLen)
if len < 0:
raiseOSError(osLastError(), symlinkPath)
if len > maxSymlinkLen:
result = newString(len+1)
len = readlink(symlinkPath, result, len)
setLen(result, len)
const hasCCopyfile = defined(osx) and not defined(nimLegacyCopyFile) const hasCCopyfile = defined(osx) and not defined(nimLegacyCopyFile)
# xxx instead of `nimLegacyCopyFile`, support something like: `when osxVersion >= (10, 5)` # xxx instead of `nimLegacyCopyFile`, support something like: `when osxVersion >= (10, 5)`
@ -1652,10 +1713,22 @@ when hasCCopyfile:
COPYFILE_XATTR {.nodecl.}: copyfile_flags_t COPYFILE_XATTR {.nodecl.}: copyfile_flags_t
{.pop.} {.pop.}
proc copyFile*(source, dest: string) {.rtl, extern: "nos$1", type CopyFlag* = enum ## Copy options.
tags: [ReadIOEffect, WriteIOEffect], noWeirdTarget.} = cfSymlinkAsIs, ## Copy symlinks as symlinks
cfSymlinkFollow, ## Copy the files symlinks point to
cfSymlinkIgnore ## Ignore symlinks
const copyFlagSymlink = {cfSymlinkAsIs, cfSymlinkFollow, cfSymlinkIgnore}
proc copyFile*(source, dest: string, options = {cfSymlinkFollow}) {.rtl,
extern: "nos$1", tags: [ReadDirEffect, ReadIOEffect, WriteIOEffect],
noWeirdTarget.} =
## Copies a file from `source` to `dest`, where `dest.parentDir` must exist. ## Copies a file from `source` to `dest`, where `dest.parentDir` must exist.
## ##
## On non-Windows OSes, `options` specify the way file is copied; by default,
## if `source` is a symlink, copies the file symlink points to. `options` is
## ignored on Windows: symlinks are skipped.
##
## If this fails, `OSError` is raised. ## If this fails, `OSError` is raised.
## ##
## On the Windows platform this proc will ## On the Windows platform this proc will
@ -1663,7 +1736,8 @@ proc copyFile*(source, dest: string) {.rtl, extern: "nos$1",
## ##
## On other platforms you need ## On other platforms you need
## to use `getFilePermissions <#getFilePermissions,string>`_ and ## to use `getFilePermissions <#getFilePermissions,string>`_ and
## `setFilePermissions <#setFilePermissions,string,set[FilePermission]>`_ procs ## `setFilePermissions <#setFilePermissions,string,set[FilePermission]>`_
## procs
## to copy them by hand (or use the convenience `copyFileWithPermissions ## to copy them by hand (or use the convenience `copyFileWithPermissions
## proc <#copyFileWithPermissions,string,string>`_), ## proc <#copyFileWithPermissions,string,string>`_),
## otherwise `dest` will inherit the default permissions of a newly ## otherwise `dest` will inherit the default permissions of a newly
@ -1676,58 +1750,81 @@ proc copyFile*(source, dest: string) {.rtl, extern: "nos$1",
## `-d:nimLegacyCopyFile` is used. ## `-d:nimLegacyCopyFile` is used.
## ##
## See also: ## See also:
## * `CopyFlag enum <#CopyFlag>`_
## * `copyDir proc <#copyDir,string,string>`_ ## * `copyDir proc <#copyDir,string,string>`_
## * `copyFileWithPermissions proc <#copyFileWithPermissions,string,string>`_ ## * `copyFileWithPermissions proc <#copyFileWithPermissions,string,string>`_
## * `tryRemoveFile proc <#tryRemoveFile,string>`_ ## * `tryRemoveFile proc <#tryRemoveFile,string>`_
## * `removeFile proc <#removeFile,string>`_ ## * `removeFile proc <#removeFile,string>`_
## * `moveFile proc <#moveFile,string,string>`_ ## * `moveFile proc <#moveFile,string,string>`_
doAssert card(copyFlagSymlink * options) == 1, "There should be exactly " &
"one cfSymlink* in options"
let isSymlink = source.symlinkExists
if isSymlink and (cfSymlinkIgnore in options or defined(windows)):
return
when defined(Windows): when defined(Windows):
when useWinUnicode: when useWinUnicode:
let s = newWideCString(source) let s = newWideCString(source)
let d = newWideCString(dest) let d = newWideCString(dest)
if copyFileW(s, d, 0'i32) == 0'i32: raiseOSError(osLastError(), $(source, dest)) if copyFileW(s, d, 0'i32) == 0'i32:
raiseOSError(osLastError(), $(source, dest))
else: else:
if copyFileA(source, dest, 0'i32) == 0'i32: raiseOSError(osLastError(), $(source, dest)) if copyFileA(source, dest, 0'i32) == 0'i32:
elif hasCCopyfile: raiseOSError(osLastError(), $(source, dest))
let state = copyfile_state_alloc()
# xxx `COPYFILE_STAT` could be used for one-shot `copyFileWithPermissions`.
let status = c_copyfile(source.cstring, dest.cstring, state, COPYFILE_DATA)
if status != 0:
let err = osLastError()
discard copyfile_state_free(state)
raiseOSError(err, $(source, dest))
let status2 = copyfile_state_free(state)
if status2 != 0: raiseOSError(osLastError(), $(source, dest))
else: else:
# generic version of copyFile which works for any platform: if isSymlink and cfSymlinkAsIs in options:
const bufSize = 8000 # better for memory manager createSymlink(expandSymlink(source), dest)
var d, s: File else:
if not open(s, source): raiseOSError(osLastError(), source) when hasCCopyfile:
if not open(d, dest, fmWrite): let state = copyfile_state_alloc()
close(s) # xxx `COPYFILE_STAT` could be used for one-shot
raiseOSError(osLastError(), dest) # `copyFileWithPermissions`.
var buf = alloc(bufSize) let status = c_copyfile(source.cstring, dest.cstring, state,
while true: COPYFILE_DATA)
var bytesread = readBuffer(s, buf, bufSize) if status != 0:
if bytesread > 0: let err = osLastError()
var byteswritten = writeBuffer(d, buf, bytesread) discard copyfile_state_free(state)
if bytesread != byteswritten: raiseOSError(err, $(source, dest))
dealloc(buf) let status2 = copyfile_state_free(state)
if status2 != 0: raiseOSError(osLastError(), $(source, dest))
else:
# generic version of copyFile which works for any platform:
const bufSize = 8000 # better for memory manager
var d, s: File
if not open(s, source):raiseOSError(osLastError(), source)
if not open(d, dest, fmWrite):
close(s) close(s)
close(d)
raiseOSError(osLastError(), dest) raiseOSError(osLastError(), dest)
if bytesread != bufSize: break var buf = alloc(bufSize)
dealloc(buf) while true:
close(s) var bytesread = readBuffer(s, buf, bufSize)
flushFile(d) if bytesread > 0:
close(d) var byteswritten = writeBuffer(d, buf, bytesread)
if bytesread != byteswritten:
dealloc(buf)
close(s)
close(d)
raiseOSError(osLastError(), dest)
if bytesread != bufSize: break
dealloc(buf)
close(s)
flushFile(d)
close(d)
proc copyFileToDir*(source, dir: string) {.noWeirdTarget, since: (1,3,7).} = proc copyFileToDir*(source, dir: string, options = {cfSymlinkFollow})
{.noWeirdTarget, since: (1,3,7).} =
## Copies a file `source` into directory `dir`, which must exist. ## Copies a file `source` into directory `dir`, which must exist.
##
## On non-Windows OSes, `options` specify the way file is copied; by default,
## if `source` is a symlink, copies the file symlink points to. `options` is
## ignored on Windows: symlinks are skipped.
##
## See also:
## * `CopyFlag enum <#CopyFlag>`_
## * `copyFile proc <#copyDir,string,string>`_
if dir.len == 0: # treating "" as "." is error prone if dir.len == 0: # treating "" as "." is error prone
raise newException(ValueError, "dest is empty") raise newException(ValueError, "dest is empty")
copyFile(source, dir / source.lastPathPart) copyFile(source, dir / source.lastPathPart, options)
when not declared(ENOENT) and not defined(Windows): when not declared(ENOENT) and not defined(Windows):
when NoFakeVars: when NoFakeVars:
@ -1821,9 +1918,12 @@ proc tryMoveFSObject(source, dest: string): bool {.noWeirdTarget.} =
return true return true
proc moveFile*(source, dest: string) {.rtl, extern: "nos$1", proc moveFile*(source, dest: string) {.rtl, extern: "nos$1",
tags: [ReadIOEffect, WriteIOEffect], noWeirdTarget.} = tags: [ReadDirEffect, ReadIOEffect, WriteIOEffect], noWeirdTarget.} =
## Moves a file from `source` to `dest`. ## Moves a file from `source` to `dest`.
## ##
## Symlinks are not followed: if `source` is a symlink, it is itself moved,
## not its target.
##
## If this fails, `OSError` is raised. ## If this fails, `OSError` is raised.
## If `dest` already exists, it will be overwritten. ## If `dest` already exists, it will be overwritten.
## ##
@ -1839,7 +1939,7 @@ proc moveFile*(source, dest: string) {.rtl, extern: "nos$1",
if not tryMoveFSObject(source, dest): if not tryMoveFSObject(source, dest):
when not defined(windows): when not defined(windows):
# Fallback to copy & del # Fallback to copy & del
copyFile(source, dest) copyFile(source, dest, {cfSymlinkAsIs})
try: try:
removeFile(source) removeFile(source)
except: except:
@ -2349,9 +2449,12 @@ proc createDir*(dir: string) {.rtl, extern: "nos$1",
discard existsOrCreateDir(dir) discard existsOrCreateDir(dir)
proc copyDir*(source, dest: string) {.rtl, extern: "nos$1", proc copyDir*(source, dest: string) {.rtl, extern: "nos$1",
tags: [WriteIOEffect, ReadIOEffect], benign, noWeirdTarget.} = tags: [ReadDirEffect, WriteIOEffect, ReadIOEffect], benign, noWeirdTarget.} =
## Copies a directory from `source` to `dest`. ## Copies a directory from `source` to `dest`.
## ##
## On non-Windows OSes, symlinks are copied as symlinks. On Windows, symlinks
## are skipped.
##
## If this fails, `OSError` is raised. ## If this fails, `OSError` is raised.
## ##
## On the Windows platform this proc will copy the attributes from ## On the Windows platform this proc will copy the attributes from
@ -2373,16 +2476,17 @@ proc copyDir*(source, dest: string) {.rtl, extern: "nos$1",
createDir(dest) createDir(dest)
for kind, path in walkDir(source): for kind, path in walkDir(source):
var noSource = splitPath(path).tail var noSource = splitPath(path).tail
case kind if kind == pcDir:
of pcFile:
copyFile(path, dest / noSource)
of pcDir:
copyDir(path, dest / noSource) copyDir(path, dest / noSource)
else: discard else:
copyFile(path, dest / noSource, {cfSymlinkAsIs})
proc moveDir*(source, dest: string) {.tags: [ReadIOEffect, WriteIOEffect], noWeirdTarget.} = proc moveDir*(source, dest: string) {.tags: [ReadIOEffect, WriteIOEffect], noWeirdTarget.} =
## Moves a directory from `source` to `dest`. ## Moves a directory from `source` to `dest`.
## ##
## Symlinks are not followed: if `source` contains symlinks, they themself are
## moved, not their target.
##
## If this fails, `OSError` is raised. ## If this fails, `OSError` is raised.
## ##
## See also: ## See also:
@ -2398,34 +2502,6 @@ proc moveDir*(source, dest: string) {.tags: [ReadIOEffect, WriteIOEffect], noWei
copyDir(source, dest) copyDir(source, dest)
removeDir(source) removeDir(source)
proc createSymlink*(src, dest: string) {.noWeirdTarget.} =
## Create a symbolic link at `dest` which points to the item specified
## by `src`. On most operating systems, will fail if a link already exists.
##
## **Warning**:
## Some OS's (such as Microsoft Windows) restrict the creation
## of symlinks to root users (administrators).
##
## See also:
## * `createHardlink proc <#createHardlink,string,string>`_
## * `expandSymlink proc <#expandSymlink,string>`_
when defined(Windows):
# 2 is the SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE. This allows
# anyone with developer mode on to create a link
let flag = dirExists(src).int32 or 2
when useWinUnicode:
var wSrc = newWideCString(src)
var wDst = newWideCString(dest)
if createSymbolicLinkW(wDst, wSrc, flag) == 0 or getLastError() != 0:
raiseOSError(osLastError(), $(src, dest))
else:
if createSymbolicLinkA(dest, src, flag) == 0 or getLastError() != 0:
raiseOSError(osLastError(), $(src, dest))
else:
if symlink(src, dest) != 0:
raiseOSError(osLastError(), $(src, dest))
proc createHardlink*(src, dest: string) {.noWeirdTarget.} = proc createHardlink*(src, dest: string) {.noWeirdTarget.} =
## Create a hard link at `dest` which points to the item specified ## Create a hard link at `dest` which points to the item specified
## by `src`. ## by `src`.
@ -2449,9 +2525,14 @@ proc createHardlink*(src, dest: string) {.noWeirdTarget.} =
raiseOSError(osLastError(), $(src, dest)) raiseOSError(osLastError(), $(src, dest))
proc copyFileWithPermissions*(source, dest: string, proc copyFileWithPermissions*(source, dest: string,
ignorePermissionErrors = true) {.noWeirdTarget.} = ignorePermissionErrors = true,
options = {cfSymlinkFollow}) {.noWeirdTarget.} =
## Copies a file from `source` to `dest` preserving file permissions. ## Copies a file from `source` to `dest` preserving file permissions.
## ##
## On non-Windows OSes, `options` specify the way file is copied; by default,
## if `source` is a symlink, copies the file symlink points to. `options` is
## ignored on Windows: symlinks are skipped.
##
## This is a wrapper proc around `copyFile <#copyFile,string,string>`_, ## This is a wrapper proc around `copyFile <#copyFile,string,string>`_,
## `getFilePermissions <#getFilePermissions,string>`_ and ## `getFilePermissions <#getFilePermissions,string>`_ and
## `setFilePermissions<#setFilePermissions,string,set[FilePermission]>`_ ## `setFilePermissions<#setFilePermissions,string,set[FilePermission]>`_
@ -2467,25 +2548,31 @@ proc copyFileWithPermissions*(source, dest: string,
## `OSError`. ## `OSError`.
## ##
## See also: ## See also:
## * `CopyFlag enum <#CopyFlag>`_
## * `copyFile proc <#copyFile,string,string>`_ ## * `copyFile proc <#copyFile,string,string>`_
## * `copyDir proc <#copyDir,string,string>`_ ## * `copyDir proc <#copyDir,string,string>`_
## * `tryRemoveFile proc <#tryRemoveFile,string>`_ ## * `tryRemoveFile proc <#tryRemoveFile,string>`_
## * `removeFile proc <#removeFile,string>`_ ## * `removeFile proc <#removeFile,string>`_
## * `moveFile proc <#moveFile,string,string>`_ ## * `moveFile proc <#moveFile,string,string>`_
## * `copyDirWithPermissions proc <#copyDirWithPermissions,string,string>`_ ## * `copyDirWithPermissions proc <#copyDirWithPermissions,string,string>`_
copyFile(source, dest) copyFile(source, dest, options)
when not defined(Windows): when not defined(Windows):
try: try:
setFilePermissions(dest, getFilePermissions(source)) setFilePermissions(dest, getFilePermissions(source), followSymlinks =
(cfSymlinkFollow in options))
except: except:
if not ignorePermissionErrors: if not ignorePermissionErrors:
raise raise
proc copyDirWithPermissions*(source, dest: string, proc copyDirWithPermissions*(source, dest: string,
ignorePermissionErrors = true) {.rtl, extern: "nos$1", ignorePermissionErrors = true)
tags: [WriteIOEffect, ReadIOEffect], benign, noWeirdTarget.} = {.rtl, extern: "nos$1", tags: [ReadDirEffect, WriteIOEffect, ReadIOEffect],
benign, noWeirdTarget.} =
## Copies a directory from `source` to `dest` preserving file permissions. ## Copies a directory from `source` to `dest` preserving file permissions.
## ##
## On non-Windows OSes, symlinks are copied as symlinks. On Windows, symlinks
## are skipped.
##
## If this fails, `OSError` is raised. This is a wrapper proc around `copyDir ## If this fails, `OSError` is raised. This is a wrapper proc around `copyDir
## <#copyDir,string,string>`_ and `copyFileWithPermissions ## <#copyDir,string,string>`_ and `copyFileWithPermissions
## <#copyFileWithPermissions,string,string>`_ procs ## <#copyFileWithPermissions,string,string>`_ procs
@ -2511,18 +2598,17 @@ proc copyDirWithPermissions*(source, dest: string,
createDir(dest) createDir(dest)
when not defined(Windows): when not defined(Windows):
try: try:
setFilePermissions(dest, getFilePermissions(source)) setFilePermissions(dest, getFilePermissions(source), followSymlinks =
false)
except: except:
if not ignorePermissionErrors: if not ignorePermissionErrors:
raise raise
for kind, path in walkDir(source): for kind, path in walkDir(source):
var noSource = splitPath(path).tail var noSource = splitPath(path).tail
case kind if kind == pcDir:
of pcFile:
copyFileWithPermissions(path, dest / noSource, ignorePermissionErrors)
of pcDir:
copyDirWithPermissions(path, dest / noSource, ignorePermissionErrors) copyDirWithPermissions(path, dest / noSource, ignorePermissionErrors)
else: discard else:
copyFileWithPermissions(path, dest / noSource, ignorePermissionErrors, {cfSymlinkAsIs})
proc inclFilePermissions*(filename: string, proc inclFilePermissions*(filename: string,
permissions: set[FilePermission]) {. permissions: set[FilePermission]) {.
@ -2542,25 +2628,6 @@ proc exclFilePermissions*(filename: string,
## setFilePermissions(filename, getFilePermissions(filename)-permissions) ## setFilePermissions(filename, getFilePermissions(filename)-permissions)
setFilePermissions(filename, getFilePermissions(filename)-permissions) setFilePermissions(filename, getFilePermissions(filename)-permissions)
proc expandSymlink*(symlinkPath: string): string {.noWeirdTarget.} =
## Returns a string representing the path to which the symbolic link points.
##
## On Windows this is a noop, ``symlinkPath`` is simply returned.
##
## See also:
## * `createSymlink proc <#createSymlink,string,string>`_
when defined(windows):
result = symlinkPath
else:
result = newString(maxSymlinkLen)
var len = readlink(symlinkPath, result, maxSymlinkLen)
if len < 0:
raiseOSError(osLastError(), symlinkPath)
if len > maxSymlinkLen:
result = newString(len+1)
len = readlink(symlinkPath, result, len)
setLen(result, len)
proc parseCmdLine*(c: string): seq[string] {. proc parseCmdLine*(c: string): seq[string] {.
noSideEffect, rtl, extern: "nos$1".} = noSideEffect, rtl, extern: "nos$1".} =
## Splits a `command line`:idx: into several components. ## Splits a `command line`:idx: into several components.

View file

@ -26,6 +26,7 @@ Raises
# test os path creation, iteration, and deletion # test os path creation, iteration, and deletion
import os, strutils, pathnorm import os, strutils, pathnorm
from stdtest/specialpaths import buildDir
block fileOperations: block fileOperations:
let files = @["these.txt", "are.x", "testing.r", "files.q"] let files = @["these.txt", "are.x", "testing.r", "files.q"]
@ -154,6 +155,112 @@ block fileOperations:
doAssert fileExists("../dest/a/file.txt") doAssert fileExists("../dest/a/file.txt")
removeDir("../dest") removeDir("../dest")
# Symlink handling in `copyFile`, `copyFileWithPermissions`, `copyFileToDir`,
# `copyDir`, `copyDirWithPermissions`, `moveFile`, and `moveDir`.
block:
const symlinksAreHandled = not defined(windows)
const dname = buildDir/"D20210116T140629"
const subDir = dname/"sub"
const subDir2 = dname/"sub2"
const brokenSymlinkName = "D20210101T191320_BROKEN_SYMLINK"
const brokenSymlink = dname/brokenSymlinkName
const brokenSymlinkSrc = "D20210101T191320_nonexistant"
const brokenSymlinkCopy = brokenSymlink & "_COPY"
const brokenSymlinkInSubDir = subDir/brokenSymlinkName
const brokenSymlinkInSubDir2 = subDir2/brokenSymlinkName
createDir(subDir)
createSymlink(brokenSymlinkSrc, brokenSymlink)
# Test copyFile
when symlinksAreHandled:
doAssertRaises(OSError):
copyFile(brokenSymlink, brokenSymlinkCopy)
doAssertRaises(OSError):
copyFile(brokenSymlink, brokenSymlinkCopy, {cfSymlinkFollow})
copyFile(brokenSymlink, brokenSymlinkCopy, {cfSymlinkIgnore})
doAssert not fileExists(brokenSymlinkCopy)
copyFile(brokenSymlink, brokenSymlinkCopy, {cfSymlinkAsIs})
when symlinksAreHandled:
doAssert expandSymlink(brokenSymlinkCopy) == brokenSymlinkSrc
removeFile(brokenSymlinkCopy)
else:
doAssert not fileExists(brokenSymlinkCopy)
doAssertRaises(AssertionDefect):
copyFile(brokenSymlink, brokenSymlinkCopy,
{cfSymlinkAsIs, cfSymlinkFollow})
# Test copyFileWithPermissions
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})
# Test copyFileToDir
when symlinksAreHandled:
doAssertRaises(OSError):
copyFileToDir(brokenSymlink, subDir)
doAssertRaises(OSError):
copyFileToDir(brokenSymlink, subDir, {cfSymlinkFollow})
copyFileToDir(brokenSymlink, subDir, {cfSymlinkIgnore})
doAssert not fileExists(brokenSymlinkInSubDir)
copyFileToDir(brokenSymlink, subDir, {cfSymlinkAsIs})
when symlinksAreHandled:
doAssert expandSymlink(brokenSymlinkInSubDir) == brokenSymlinkSrc
removeFile(brokenSymlinkInSubDir)
else:
doAssert not fileExists(brokenSymlinkInSubDir)
createSymlink(brokenSymlinkSrc, brokenSymlinkInSubDir)
# Test copyDir
copyDir(subDir, subDir2)
when symlinksAreHandled:
doAssert expandSymlink(brokenSymlinkInSubDir2) == brokenSymlinkSrc
else:
doAssert not fileExists(brokenSymlinkInSubDir2)
removeDir(subDir2)
# Test copyDirWithPermissions
copyDirWithPermissions(subDir, subDir2)
when symlinksAreHandled:
doAssert expandSymlink(brokenSymlinkInSubDir2) == brokenSymlinkSrc
else:
doAssert not fileExists(brokenSymlinkInSubDir2)
removeDir(subDir2)
# Test moveFile
moveFile(brokenSymlink, brokenSymlinkCopy)
when not defined(windows):
doAssert expandSymlink(brokenSymlinkCopy) == brokenSymlinkSrc
else:
doAssert symlinkExists(brokenSymlinkCopy)
removeFile(brokenSymlinkCopy)
# Test moveDir
moveDir(subDir, subDir2)
when not defined(windows):
doAssert expandSymlink(brokenSymlinkInSubDir2) == brokenSymlinkSrc
else:
doAssert symlinkExists(brokenSymlinkInSubDir2)
removeDir(dname)
import times import times
block modificationTime: block modificationTime:
# Test get/set modification times # Test get/set modification times

View file

@ -3,6 +3,8 @@
{.warning[UnusedImport]: off.} {.warning[UnusedImport]: off.}
from stdtest/specialpaths import buildDir
import std/[ import std/[
# Core: # Core:
bitops, typetraits, lenientops, macros, volatile, bitops, typetraits, lenientops, macros, volatile,
@ -87,3 +89,28 @@ block:
try: doAssert false try: doAssert false
except Exception as e: except Exception as e:
discard discard
block: # cpDir, cpFile, dirExists, fileExists, mkDir, mvDir, mvFile, rmDir, rmFile
const dname = buildDir/"D20210121T175016"
const subDir = dname/"sub"
const subDir2 = dname/"sub2"
const fpath = subDir/"f"
const fpath2 = subDir/"f2"
const fpath3 = subDir2/"f"
mkDir(subDir)
writeFile(fpath, "some text")
cpFile(fpath, fpath2)
doAssert fileExists(fpath2)
rmFile(fpath2)
cpDir(subDir, subDir2)
doAssert fileExists(fpath3)
rmDir(subDir2)
mvFile(fpath, fpath2)
doAssert not fileExists(fpath)
doAssert fileExists(fpath2)
mvFile(fpath2, fpath)
mvDir(subDir, subDir2)
doAssert not dirExists(subDir)
doAssert dirExists(subDir2)
mvDir(subDir2, subDir)
rmDir(dname)