Skip to content

Commit 92621c8

Browse files
author
Roman Inflianskas
committed
stdlib/os: Add followSymlinks = true to copy/move functions
Sometimes there is a need to copy symlinks as they are. This commit adds optional argument `followSymlinks = true` to all copy/move functions in os module. Note: Inspired by https://docs.python.org/3/library/shutil.html#shutil.copy
1 parent 165d397 commit 92621c8

File tree

2 files changed

+91
-74
lines changed

2 files changed

+91
-74
lines changed

lib/pure/os.nim

Lines changed: 81 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -1177,7 +1177,7 @@ const
11771177
## On Windows ``["exe", "cmd", "bat"]``, on Posix ``[""]``.
11781178
when defined(windows): ["exe", "cmd", "bat"] else: [""]
11791179

1180-
proc findExe*(exe: string, followSymlinks: bool = true;
1180+
proc findExe*(exe: string, followSymlinks = true;
11811181
extensions: openArray[string]=ExeExts): string {.
11821182
tags: [ReadDirEffect, ReadEnvEffect, ReadIOEffect], noNimJs.} =
11831183
## Searches for `exe` in the current working directory and then
@@ -1647,8 +1647,11 @@ proc setFilePermissions*(filename: string, permissions: set[FilePermission]) {.
16471647
var res2 = setFileAttributesA(filename, res)
16481648
if res2 == - 1'i32: raiseOSError(osLastError(), $(filename, permissions))
16491649

1650-
proc copyFile*(source, dest: string) {.rtl, extern: "nos$1",
1651-
tags: [ReadIOEffect, WriteIOEffect], noWeirdTarget.} =
1650+
proc createSymlink*(src, dest: string) {.tags: [ReadDirEffect, WriteIOEffect], noWeirdTarget.}
1651+
proc expandSymlink*(symlinkPath: string): string {.tags: [ReadIOEffect], noWeirdTarget.}
1652+
1653+
proc copyFile*(source, dest: string, followSymlinks = true) {.rtl, extern: "nos$1",
1654+
tags: [ReadDirEffect, ReadIOEffect, WriteIOEffect], noWeirdTarget.} =
16521655
## Copies a file from `source` to `dest`, where `dest.parentDir` must exist.
16531656
##
16541657
## If this fails, `OSError` is raised.
@@ -1668,11 +1671,11 @@ proc copyFile*(source, dest: string) {.rtl, extern: "nos$1",
16681671
## will be preserved and the content overwritten.
16691672
##
16701673
## See also:
1671-
## * `copyDir proc <#copyDir,string,string>`_
1674+
## * `copyDir proc <#copyDir,string,string,bool>`_
16721675
## * `copyFileWithPermissions proc <#copyFileWithPermissions,string,string>`_
16731676
## * `tryRemoveFile proc <#tryRemoveFile,string>`_
16741677
## * `removeFile proc <#removeFile,string>`_
1675-
## * `moveFile proc <#moveFile,string,string>`_
1678+
## * `moveFile proc <#moveFile,string,string,bool>`_
16761679

16771680
when defined(Windows):
16781681
when useWinUnicode:
@@ -1682,34 +1685,37 @@ proc copyFile*(source, dest: string) {.rtl, extern: "nos$1",
16821685
else:
16831686
if copyFileA(source, dest, 0'i32) == 0'i32: raiseOSError(osLastError(), $(source, dest))
16841687
else:
1685-
# generic version of copyFile which works for any platform:
1686-
const bufSize = 8000 # better for memory manager
1687-
var d, s: File
1688-
if not open(s, source): raiseOSError(osLastError(), source)
1689-
if not open(d, dest, fmWrite):
1688+
if not followSymlinks and source.checkSymlink:
1689+
createSymlink(expandSymlink(source), dest)
1690+
else:
1691+
# generic version of copyFile which works for any platform:
1692+
const bufSize = 8000 # better for memory manager
1693+
var d, s: File
1694+
if not open(s, source): raiseOSError(osLastError(), source)
1695+
if not open(d, dest, fmWrite):
1696+
close(s)
1697+
raiseOSError(osLastError(), dest)
1698+
var buf = alloc(bufSize)
1699+
while true:
1700+
var bytesread = readBuffer(s, buf, bufSize)
1701+
if bytesread > 0:
1702+
var byteswritten = writeBuffer(d, buf, bytesread)
1703+
if bytesread != byteswritten:
1704+
dealloc(buf)
1705+
close(s)
1706+
close(d)
1707+
raiseOSError(osLastError(), dest)
1708+
if bytesread != bufSize: break
1709+
dealloc(buf)
16901710
close(s)
1691-
raiseOSError(osLastError(), dest)
1692-
var buf = alloc(bufSize)
1693-
while true:
1694-
var bytesread = readBuffer(s, buf, bufSize)
1695-
if bytesread > 0:
1696-
var byteswritten = writeBuffer(d, buf, bytesread)
1697-
if bytesread != byteswritten:
1698-
dealloc(buf)
1699-
close(s)
1700-
close(d)
1701-
raiseOSError(osLastError(), dest)
1702-
if bytesread != bufSize: break
1703-
dealloc(buf)
1704-
close(s)
1705-
flushFile(d)
1706-
close(d)
1707-
1708-
proc copyFileToDir*(source, dir: string) {.noWeirdTarget, since: (1,3,7).} =
1711+
flushFile(d)
1712+
close(d)
1713+
1714+
proc copyFileToDir*(source, dir: string, followSymlinks = true) {.noWeirdTarget, since: (1,3,7).} =
17091715
## Copies a file `source` into directory `dir`, which must exist.
17101716
if dir.len == 0: # treating "" as "." is error prone
17111717
raise newException(ValueError, "dest is empty")
1712-
copyFile(source, dir / source.lastPathPart)
1718+
copyFile(source, dir / source.lastPathPart, followSymlinks = followSymlinks)
17131719

17141720
when not declared(ENOENT) and not defined(Windows):
17151721
when NoFakeVars:
@@ -1739,10 +1745,10 @@ proc tryRemoveFile*(file: string): bool {.rtl, extern: "nos$1", tags: [WriteDirE
17391745
## On Windows, ignores the read-only attribute.
17401746
##
17411747
## See also:
1742-
## * `copyFile proc <#copyFile,string,string>`_
1748+
## * `copyFile proc <#copyFile,string,string,bool>`_
17431749
## * `copyFileWithPermissions proc <#copyFileWithPermissions,string,string>`_
17441750
## * `removeFile proc <#removeFile,string>`_
1745-
## * `moveFile proc <#moveFile,string,string>`_
1751+
## * `moveFile proc <#moveFile,string,string,bool>`_
17461752
result = true
17471753
when defined(Windows):
17481754
when useWinUnicode:
@@ -1772,10 +1778,10 @@ proc removeFile*(file: string) {.rtl, extern: "nos$1", tags: [WriteDirEffect], n
17721778
##
17731779
## See also:
17741780
## * `removeDir proc <#removeDir,string>`_
1775-
## * `copyFile proc <#copyFile,string,string>`_
1781+
## * `copyFile proc <#copyFile,string,string,bool>`_
17761782
## * `copyFileWithPermissions proc <#copyFileWithPermissions,string,string>`_
17771783
## * `tryRemoveFile proc <#tryRemoveFile,string>`_
1778-
## * `moveFile proc <#moveFile,string,string>`_
1784+
## * `moveFile proc <#moveFile,string,string,bool>`_
17791785
if not tryRemoveFile(file):
17801786
raiseOSError(osLastError(), file)
17811787

@@ -1802,8 +1808,8 @@ proc tryMoveFSObject(source, dest: string): bool {.noWeirdTarget.} =
18021808
raiseOSError(err, $(source, dest, strerror(errno)))
18031809
return true
18041810

1805-
proc moveFile*(source, dest: string) {.rtl, extern: "nos$1",
1806-
tags: [ReadIOEffect, WriteIOEffect], noWeirdTarget.} =
1811+
proc moveFile*(source, dest: string, followSymlinks = true) {.rtl, extern: "nos$1",
1812+
tags: [ReadDirEffect, ReadIOEffect, WriteIOEffect], noWeirdTarget.} =
18071813
## Moves a file from `source` to `dest`.
18081814
##
18091815
## If this fails, `OSError` is raised.
@@ -1812,16 +1818,16 @@ proc moveFile*(source, dest: string) {.rtl, extern: "nos$1",
18121818
## Can be used to `rename files`:idx:.
18131819
##
18141820
## See also:
1815-
## * `moveDir proc <#moveDir,string,string>`_
1816-
## * `copyFile proc <#copyFile,string,string>`_
1821+
## * `moveDir proc <#moveDir,string,string,bool>`_
1822+
## * `copyFile proc <#copyFile,string,string,bool>`_
18171823
## * `copyFileWithPermissions proc <#copyFileWithPermissions,string,string>`_
18181824
## * `removeFile proc <#removeFile,string>`_
18191825
## * `tryRemoveFile proc <#tryRemoveFile,string>`_
18201826

18211827
if not tryMoveFSObject(source, dest):
18221828
when not defined(windows):
18231829
# Fallback to copy & del
1824-
copyFile(source, dest)
1830+
copyFile(source, dest, followSymlinks = followSymlinks)
18251831
try:
18261832
removeFile(source)
18271833
except:
@@ -2223,9 +2229,9 @@ proc removeDir*(dir: string, checkDir = false) {.rtl, extern: "nos$1", tags: [
22232229
## * `removeFile proc <#removeFile,string>`_
22242230
## * `existsOrCreateDir proc <#existsOrCreateDir,string>`_
22252231
## * `createDir proc <#createDir,string>`_
2226-
## * `copyDir proc <#copyDir,string,string>`_
2227-
## * `copyDirWithPermissions proc <#copyDirWithPermissions,string,string>`_
2228-
## * `moveDir proc <#moveDir,string,string>`_
2232+
## * `copyDir proc <#copyDir,string,string,bool>`_
2233+
## * `copyDirWithPermissions proc <#copyDirWithPermissions,string,string,bool>`_
2234+
## * `moveDir proc <#moveDir,string,string,bool>`_
22292235
for kind, path in walkDir(dir, checkDir = checkDir):
22302236
case kind
22312237
of pcFile, pcLinkToFile, pcLinkToDir: removeFile(path)
@@ -2290,9 +2296,9 @@ proc existsOrCreateDir*(dir: string): bool {.rtl, extern: "nos$1",
22902296
## See also:
22912297
## * `removeDir proc <#removeDir,string>`_
22922298
## * `createDir proc <#createDir,string>`_
2293-
## * `copyDir proc <#copyDir,string,string>`_
2294-
## * `copyDirWithPermissions proc <#copyDirWithPermissions,string,string>`_
2295-
## * `moveDir proc <#moveDir,string,string>`_
2299+
## * `copyDir proc <#copyDir,string,string,bool>`_
2300+
## * `copyDirWithPermissions proc <#copyDirWithPermissions,string,string,bool>`_
2301+
## * `moveDir proc <#moveDir,string,string,bool>`_
22962302
result = not rawCreateDir(dir)
22972303
if result:
22982304
# path already exists - need to check that it is indeed a directory
@@ -2312,9 +2318,9 @@ proc createDir*(dir: string) {.rtl, extern: "nos$1",
23122318
## See also:
23132319
## * `removeDir proc <#removeDir,string>`_
23142320
## * `existsOrCreateDir proc <#existsOrCreateDir,string>`_
2315-
## * `copyDir proc <#copyDir,string,string>`_
2316-
## * `copyDirWithPermissions proc <#copyDirWithPermissions,string,string>`_
2317-
## * `moveDir proc <#moveDir,string,string>`_
2321+
## * `copyDir proc <#copyDir,string,string,bool>`_
2322+
## * `copyDirWithPermissions proc <#copyDirWithPermissions,string,string,bool>`_
2323+
## * `moveDir proc <#moveDir,string,string,bool>`_
23182324
var omitNext = false
23192325
when doslikeFileSystem:
23202326
omitNext = isAbsolute(dir)
@@ -2330,8 +2336,8 @@ proc createDir*(dir: string) {.rtl, extern: "nos$1",
23302336
dir[^1] notin {DirSep, AltSep}:
23312337
discard existsOrCreateDir(dir)
23322338

2333-
proc copyDir*(source, dest: string) {.rtl, extern: "nos$1",
2334-
tags: [WriteIOEffect, ReadIOEffect], benign, noWeirdTarget.} =
2339+
proc copyDir*(source, dest: string, followSymlinks = true) {.rtl, extern: "nos$1",
2340+
tags: [ReadDirEffect, ReadIOEffect, WriteIOEffect], benign, noWeirdTarget.} =
23352341
## Copies a directory from `source` to `dest`.
23362342
##
23372343
## If this fails, `OSError` is raised.
@@ -2341,46 +2347,47 @@ proc copyDir*(source, dest: string) {.rtl, extern: "nos$1",
23412347
##
23422348
## On other platforms created files and directories will inherit the
23432349
## default permissions of a newly created file/directory for the user.
2344-
## Use `copyDirWithPermissions proc <#copyDirWithPermissions,string,string>`_
2350+
## Use `copyDirWithPermissions proc <#copyDirWithPermissions,string,string,bool>`_
23452351
## to preserve attributes recursively on these platforms.
23462352
##
23472353
## See also:
2348-
## * `copyDirWithPermissions proc <#copyDirWithPermissions,string,string>`_
2349-
## * `copyFile proc <#copyFile,string,string>`_
2354+
## * `copyDirWithPermissions proc <#copyDirWithPermissions,string,string,bool>`_
2355+
## * `copyFile proc <#copyFile,string,string,bool>`_
23502356
## * `copyFileWithPermissions proc <#copyFileWithPermissions,string,string>`_
23512357
## * `removeDir proc <#removeDir,string>`_
23522358
## * `existsOrCreateDir proc <#existsOrCreateDir,string>`_
23532359
## * `createDir proc <#createDir,string>`_
2354-
## * `moveDir proc <#moveDir,string,string>`_
2360+
## * `moveDir proc <#moveDir,string,string,bool>`_
23552361
createDir(dest)
23562362
for kind, path in walkDir(source):
23572363
var noSource = splitPath(path).tail
23582364
case kind
23592365
of pcFile:
2360-
copyFile(path, dest / noSource)
2366+
copyFile(path, dest / noSource, followSymlinks = followSymlinks)
23612367
of pcDir:
2362-
copyDir(path, dest / noSource)
2368+
copyDir(path, dest / noSource, followSymlinks = followSymlinks)
23632369
else: discard
23642370

2365-
proc moveDir*(source, dest: string) {.tags: [ReadIOEffect, WriteIOEffect], noWeirdTarget.} =
2371+
proc moveDir*(source, dest: string, followSymlinks = true) {.tags: [ReadDirEffect, ReadIOEffect, WriteIOEffect],
2372+
noWeirdTarget.} =
23662373
## Moves a directory from `source` to `dest`.
23672374
##
23682375
## If this fails, `OSError` is raised.
23692376
##
23702377
## See also:
2371-
## * `moveFile proc <#moveFile,string,string>`_
2372-
## * `copyDir proc <#copyDir,string,string>`_
2373-
## * `copyDirWithPermissions proc <#copyDirWithPermissions,string,string>`_
2378+
## * `moveFile proc <#moveFile,string,string,bool>`_
2379+
## * `copyDir proc <#copyDir,string,string,bool>`_
2380+
## * `copyDirWithPermissions proc <#copyDirWithPermissions,string,string,bool>`_
23742381
## * `removeDir proc <#removeDir,string>`_
23752382
## * `existsOrCreateDir proc <#existsOrCreateDir,string>`_
23762383
## * `createDir proc <#createDir,string>`_
23772384
if not tryMoveFSObject(source, dest):
23782385
when not defined(windows):
23792386
# Fallback to copy & del
2380-
copyDir(source, dest)
2387+
copyDir(source, dest, followSymlinks = followSymlinks)
23812388
removeDir(source)
23822389

2383-
proc createSymlink*(src, dest: string) {.noWeirdTarget.} =
2390+
proc createSymlink*(src, dest: string) {.tags: [ReadDirEffect, WriteIOEffect], noWeirdTarget.} =
23842391
## Create a symbolic link at `dest` which points to the item specified
23852392
## by `src`. On most operating systems, will fail if a link already exists.
23862393
##
@@ -2431,7 +2438,7 @@ proc createHardlink*(src, dest: string) {.noWeirdTarget.} =
24312438
raiseOSError(osLastError(), $(src, dest))
24322439

24332440
proc copyFileWithPermissions*(source, dest: string,
2434-
ignorePermissionErrors = true) {.noWeirdTarget.} =
2441+
ignorePermissionErrors = true, followSymlinks = true) {.noWeirdTarget.} =
24352442
## Copies a file from `source` to `dest` preserving file permissions.
24362443
##
24372444
## This is a wrapper proc around `copyFile <#copyFile,string,string>`_,
@@ -2450,12 +2457,12 @@ proc copyFileWithPermissions*(source, dest: string,
24502457
##
24512458
## See also:
24522459
## * `copyFile proc <#copyFile,string,string>`_
2453-
## * `copyDir proc <#copyDir,string,string>`_
2460+
## * `copyDir proc <#copyDir,string,string,bool>`_
24542461
## * `tryRemoveFile proc <#tryRemoveFile,string>`_
24552462
## * `removeFile proc <#removeFile,string>`_
2456-
## * `moveFile proc <#moveFile,string,string>`_
2457-
## * `copyDirWithPermissions proc <#copyDirWithPermissions,string,string>`_
2458-
copyFile(source, dest)
2463+
## * `moveFile proc <#moveFile,string,string,bool>`_
2464+
## * `copyDirWithPermissions proc <#copyDirWithPermissions,string,string,bool>`_
2465+
copyFile(source, dest, followSymlinks = followSymlinks)
24592466
when not defined(Windows):
24602467
try:
24612468
setFilePermissions(dest, getFilePermissions(source))
@@ -2464,17 +2471,17 @@ proc copyFileWithPermissions*(source, dest: string,
24642471
raise
24652472

24662473
proc copyDirWithPermissions*(source, dest: string,
2467-
ignorePermissionErrors = true) {.rtl, extern: "nos$1",
2474+
ignorePermissionErrors = true, followSymlinks = true) {.rtl, extern: "nos$1",
24682475
tags: [WriteIOEffect, ReadIOEffect], benign, noWeirdTarget.} =
24692476
## Copies a directory from `source` to `dest` preserving file permissions.
24702477
##
24712478
## If this fails, `OSError` is raised. This is a wrapper proc around `copyDir
2472-
## <#copyDir,string,string>`_ and `copyFileWithPermissions
2479+
## <#copyDir,string,string,bool>`_ and `copyFileWithPermissions
24732480
## <#copyFileWithPermissions,string,string>`_ procs
24742481
## on non-Windows platforms.
24752482
##
24762483
## On Windows this proc is just a wrapper for `copyDir proc
2477-
## <#copyDir,string,string>`_ since that proc already copies attributes.
2484+
## <#copyDir,string,string,bool>`_ since that proc already copies attributes.
24782485
##
24792486
## On non-Windows systems permissions are copied after the file or directory
24802487
## itself has been copied, which won't happen atomically and could lead to a
@@ -2483,11 +2490,11 @@ proc copyDirWithPermissions*(source, dest: string,
24832490
## `OSError`.
24842491
##
24852492
## See also:
2486-
## * `copyDir proc <#copyDir,string,string>`_
2493+
## * `copyDir proc <#copyDir,string,string,bool>`_
24872494
## * `copyFile proc <#copyFile,string,string>`_
24882495
## * `copyFileWithPermissions proc <#copyFileWithPermissions,string,string>`_
24892496
## * `removeDir proc <#removeDir,string>`_
2490-
## * `moveDir proc <#moveDir,string,string>`_
2497+
## * `moveDir proc <#moveDir,string,string,bool>`_
24912498
## * `existsOrCreateDir proc <#existsOrCreateDir,string>`_
24922499
## * `createDir proc <#createDir,string>`_
24932500
createDir(dest)
@@ -2501,9 +2508,9 @@ proc copyDirWithPermissions*(source, dest: string,
25012508
var noSource = splitPath(path).tail
25022509
case kind
25032510
of pcFile:
2504-
copyFileWithPermissions(path, dest / noSource, ignorePermissionErrors)
2511+
copyFileWithPermissions(path, dest / noSource, ignorePermissionErrors, followSymlinks = followSymlinks)
25052512
of pcDir:
2506-
copyDirWithPermissions(path, dest / noSource, ignorePermissionErrors)
2513+
copyDirWithPermissions(path, dest / noSource, ignorePermissionErrors, followSymlinks = followSymlinks)
25072514
else: discard
25082515

25092516
proc inclFilePermissions*(filename: string,
@@ -2524,7 +2531,7 @@ proc exclFilePermissions*(filename: string,
25242531
## setFilePermissions(filename, getFilePermissions(filename)-permissions)
25252532
setFilePermissions(filename, getFilePermissions(filename)-permissions)
25262533

2527-
proc expandSymlink*(symlinkPath: string): string {.noWeirdTarget.} =
2534+
proc expandSymlink*(symlinkPath: string): string {.tags: [ReadIOEffect], noWeirdTarget.} =
25282535
## Returns a string representing the path to which the symbolic link points.
25292536
##
25302537
## On Windows this is a noop, ``symlinkPath`` is simply returned.

tests/stdlib/tos.nim

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,16 @@ block fileOperations:
5454
doAssert not dirExists(dname/sub)
5555
removeFile(dname/fname)
5656
removeFile(dname/fname2)
57+
when not defined(windows):
58+
let brokenSymlink = dname/"D20210101T191320_BROKEN_SYMLINK"
59+
let brokenSymlinkDesc = "D20210101T191320_I_DO_NOT_EXIST"
60+
createSymlink(brokenSymlinkDesc, brokenSymlink)
61+
let brokenSymlinkCopy = brokenSymlink & "_COPY"
62+
doAssertRaises(OSError): copyFile(brokenSymlink, brokenSymlinkCopy)
63+
copyFile(brokenSymlink, brokenSymlinkCopy, followSymlinks = false)
64+
doAssert expandSymlink(brokenSymlinkCopy) == brokenSymlinkDesc
65+
removeFile(brokenSymlink)
66+
removeFile(brokenSymlinkCopy)
5767

5868
# Test creating files and dirs
5969
for dir in dirs:

0 commit comments

Comments
 (0)