From e194c7cc87136f7cf68ac70f921210ef543abbb5 Mon Sep 17 00:00:00 2001 From: ringabout <43030857+ringabout@users.noreply.github.com> Date: Fri, 1 Aug 2025 18:34:01 +0800 Subject: [PATCH] adds more functions to to `dirs` and `files` (#25083) ref https://forum.nim-lang.org/t/13272 --- changelog.md | 14 +++++ lib/std/dirs.nim | 57 ++++++++++++++++++++ lib/std/files.nim | 120 ++++++++++++++++++++++++++++++++++++++++++- tests/stdlib/tos.nim | 14 +++-- 4 files changed, 199 insertions(+), 6 deletions(-) diff --git a/changelog.md b/changelog.md index 33f36deec2..76da277edb 100644 --- a/changelog.md +++ b/changelog.md @@ -33,6 +33,20 @@ errors. - `strutils.multiReplace` overload for character set replacements in a single pass. Useful for string sanitation. Follows existing multiReplace semantics. +- `std/files` adds: + - Exports `CopyFlag` enum and `FilePermission` type for fine-grained control of file operations + - New file operation procs with `Path` support: + - `getFilePermissions`, `setFilePermissions` for managing permissions + - `tryRemoveFile` for file deletion + - `copyFile` with configurable buffer size and symlink handling + - `copyFileWithPermissions` to preserve file attributes + - `copyFileToDir` for copying files into directories + +- `std/dirs` adds: + - New directory operation procs with `Path` support: + - `copyDir` with special file handling options + - `copyDirWithPermissions` to recursively preserve attributes + - `system.setLenUninit` now supports refc, JS and VM backends. [//]: # "Changes:" diff --git a/lib/std/dirs.nim b/lib/std/dirs.nim index 380d6d08f8..9a72ba9b9a 100644 --- a/lib/std/dirs.nim +++ b/lib/std/dirs.nim @@ -4,6 +4,7 @@ from std/paths import Path, ReadDirEffect, WriteDirEffect from std/private/osdirs import dirExists, createDir, existsOrCreateDir, removeDir, moveDir, walkDir, setCurrentDir, + copyDir, copyDirWithPermissions, walkDirRec, PathComponent export PathComponent @@ -133,3 +134,59 @@ proc setCurrentDir*(newDir: Path) {.inline, tags: [].} = ## See also: ## * `getCurrentDir proc `_ osdirs.setCurrentDir(newDir.string) + +proc copyDir*(source, dest: Path; skipSpecial = false) {.inline, + tags: [ReadDirEffect, WriteIOEffect, ReadIOEffect].} = + ## Copies a directory from `source` to `dest`. + ## + ## On non-Windows OSes, symlinks are copied as symlinks. On Windows, symlinks + ## are skipped. + ## + ## If `skipSpecial` is true, then (besides all directories) only *regular* + ## files (**without** special "file" objects like FIFOs, device files, + ## etc) will be copied on Unix. + ## + ## If this fails, `OSError` is raised. + ## + ## On the Windows platform this proc will copy the attributes from + ## `source` into `dest`. + ## + ## On other platforms created files and directories will inherit the + ## default permissions of a newly created file/directory for the user. + ## Use `copyDirWithPermissions proc`_ + ## to preserve attributes recursively on these platforms. + ## + ## See also: + ## * `copyDirWithPermissions proc`_ + copyDir(source.string, dest.string, skipSpecial) + +proc copyDirWithPermissions*(source, dest: Path; + ignorePermissionErrors = true, + skipSpecial = false) + {.inline, tags: [ReadDirEffect, WriteIOEffect, ReadIOEffect].} = + ## 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 `skipSpecial` is true, then (besides all directories) only *regular* + ## files (**without** special "file" objects like FIFOs, device files, + ## etc) will be copied on Unix. + ## + ## If this fails, `OSError` is raised. This is a wrapper proc around + ## `copyDir`_ and `copyFileWithPermissions`_ procs + ## on non-Windows platforms. + ## + ## On Windows this proc is just a wrapper for `copyDir proc`_ since + ## that proc already copies attributes. + ## + ## On non-Windows systems permissions are copied after the file or directory + ## itself has been copied, which won't happen atomically and could lead to a + ## race condition. If `ignorePermissionErrors` is true (default), errors while + ## reading/setting file attributes will be ignored, otherwise will raise + ## `OSError`. + ## + ## See also: + ## * `copyDir proc`_ + copyDirWithPermissions(source.string, dest.string, + ignorePermissionErrors, skipSpecial) diff --git a/lib/std/files.nim b/lib/std/files.nim index c4e0491c99..223e51b6cc 100644 --- a/lib/std/files.nim +++ b/lib/std/files.nim @@ -6,8 +6,43 @@ from std/paths import Path, ReadDirEffect, WriteDirEffect from std/private/osfiles import fileExists, removeFile, - moveFile + moveFile, copyFile, copyFileWithPermissions, + copyFileToDir, tryRemoveFile, + getFilePermissions, setFilePermissions, + CopyFlag, FilePermission +export CopyFlag, FilePermission + + +proc getFilePermissions*(filename: Path): set[FilePermission] {.inline, tags: [ReadDirEffect].} = + ## Retrieves file permissions for `filename`. + ## + ## `OSError` is raised in case of an error. + ## On Windows, only the ``readonly`` flag is checked, every other + ## permission is available in any case. + ## + ## See also: + ## * `setFilePermissions proc`_ + result = getFilePermissions(filename.string) + +proc setFilePermissions*(filename: Path, permissions: set[FilePermission], + followSymlinks = true) + {.inline, tags: [ReadDirEffect, WriteDirEffect].} = + ## 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. + ## On Windows, only the ``readonly`` flag is changed, depending on + ## ``fpUserWrite`` permission. + ## + ## See also: + ## * `getFilePermissions proc`_ + setFilePermissions(filename.string, permissions, followSymlinks) proc fileExists*(filename: Path): bool {.inline, tags: [ReadDirEffect], sideEffect.} = ## Returns true if `filename` exists and is a regular file or symlink. @@ -15,6 +50,18 @@ proc fileExists*(filename: Path): bool {.inline, tags: [ReadDirEffect], sideEffe ## Directories, device files, named pipes and sockets return false. result = fileExists(filename.string) +proc tryRemoveFile*(file: Path): bool {.inline, tags: [WriteDirEffect].} = + ## Removes the `file`. + ## + ## If this fails, returns `false`. This does not fail + ## if the file never existed in the first place. + ## + ## On Windows, ignores the read-only attribute. + ## + ## See also: + ## * `removeFile proc`_ + result = tryRemoveFile(file.string) + proc removeFile*(file: Path) {.inline, tags: [WriteDirEffect].} = ## Removes the `file`. ## @@ -26,6 +73,7 @@ proc removeFile*(file: Path) {.inline, tags: [WriteDirEffect].} = ## See also: ## * `removeDir proc `_ ## * `moveFile proc`_ + ## * `tryRemoveFile proc`_ removeFile(file.string) proc moveFile*(source, dest: Path) {.inline, @@ -44,3 +92,73 @@ proc moveFile*(source, dest: Path) {.inline, ## * `moveDir proc `_ ## * `removeFile proc`_ moveFile(source.string, dest.string) + +proc copyFile*(source, dest: Path; options = cfSymlinkFollow; bufferSize = 16_384) {.inline, tags: [ReadDirEffect, ReadIOEffect, WriteIOEffect].} = + ## 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. + ## + ## On the Windows platform this proc will + ## copy the source file's attributes into dest. + ## + ## On other platforms you need + ## to use `getFilePermissions`_ and + ## `setFilePermissions`_ + ## procs + ## to copy them by hand (or use the convenience `copyFileWithPermissions + ## proc`_), + ## otherwise `dest` will inherit the default permissions of a newly + ## created file for the user. + ## + ## If `dest` already exists, the file attributes + ## will be preserved and the content overwritten. + ## + ## On OSX, `copyfile` C api will be used (available since OSX 10.5) unless + ## `-d:nimLegacyCopyFile` is used. + ## + ## `copyFile` allows to specify `bufferSize` to improve I/O performance. + ## + ## See also: + ## * `copyFileWithPermissions proc`_ + copyFile(source.string, dest.string, {options}, bufferSize) + +proc copyFileWithPermissions*(source, dest: Path; + ignorePermissionErrors = true, + options = cfSymlinkFollow) {.inline.} = + ## 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`_, + ## `getFilePermissions`_ and `setFilePermissions`_ + ## procs on non-Windows platforms. + ## + ## On Windows this proc is just a wrapper for `copyFile proc`_ since + ## that proc already copies attributes. + ## + ## On non-Windows systems permissions are copied after the file itself has + ## been copied, which won't happen atomically and could lead to a race + ## condition. If `ignorePermissionErrors` is true (default), errors while + ## reading/setting file attributes will be ignored, otherwise will raise + ## `OSError`. + ## + ## See also: + ## * `copyFile proc`_ + copyFileWithPermissions(source.string, dest.string, + ignorePermissionErrors, {options}) + +proc copyFileToDir*(source, dir: Path, options = cfSymlinkFollow; bufferSize = 16_384) {.inline.} = + ## 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. + ## + ## `copyFileToDir` allows to specify `bufferSize` to improve I/O performance. + copyFileToDir(source.string, dir.string, {options}, bufferSize) diff --git a/tests/stdlib/tos.nim b/tests/stdlib/tos.nim index 611659fdbb..3b77ec3c0a 100644 --- a/tests/stdlib/tos.nim +++ b/tests/stdlib/tos.nim @@ -30,6 +30,9 @@ Raises from stdtest/specialpaths import buildDir import std/[syncio, assertions, osproc, os, strutils, pathnorm] +import std/paths except getCurrentDir +import std/[files, dirs] + block fileOperations: let files = @["these.txt", "are.x", "testing.r", "files.q"] let dirs = @["some", "created", "test", "dirs"] @@ -52,11 +55,11 @@ block fileOperations: doAssertRaises(OSError): copyFile(file, dname/sub/fname2) doAssertRaises(OSError): copyFileToDir(file, dname/sub) doAssertRaises(ValueError): copyFileToDir(file, "") - copyFile(file, file2) + copyFile(Path file, Path file2) doAssert fileExists(file2) doAssert readFile(file2) == str createDir(dname/sub) - copyFileToDir(file, dname/sub) + copyFileToDir(Path file, Path dname/sub) doAssert fileExists(dname/sub/fname) removeDir(dname/sub) doAssert not dirExists(dname/sub) @@ -131,12 +134,13 @@ block fileOperations: removeDir(dname) # test copyDir: - createDir("a/b") + createDir(Path "a/b") open("a/b/file.txt", fmWrite).close createDir("a/b/c") open("a/b/c/fileC.txt", fmWrite).close - copyDir("a", "../dest/a") + createDir(Path"a/b") + copyDir(Path "a", Path "../dest/a") removeDir("a") doAssert dirExists("../dest/a/b") @@ -169,7 +173,7 @@ block fileOperations: doAssert execCmd("mkfifo -m 600 a/fifoFile") == 0 copyDir("a/", "../dest/a/", skipSpecial = true) - copyDirWithPermissions("a/", "../dest2/a/", skipSpecial = true) + copyDirWithPermissions(Path "a/", Path "../dest2/a/", skipSpecial = true) removeDir("a") # Symlink handling in `copyFile`, `copyFileWithPermissions`, `copyFileToDir`,