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:
parent
bb85bc7ebc
commit
e9b360c5df
5 changed files with 321 additions and 106 deletions
12
changelog.md
12
changelog.md
|
|
@ -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`.
|
||||||
|
|
|
||||||
|
|
@ -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.}
|
||||||
|
|
|
||||||
279
lib/pure/os.nim
279
lib/pure/os.nim
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
Loading…
Add table
Add a link
Reference in a new issue