-
Notifications
You must be signed in to change notification settings - Fork 23
Description
Add a flexible way to handle individual items while copying (stdlib/os)
Abstract
I propose to add an additional optional (to keep source-level backward compatibility) argument to copy functions in os
module, which will control all aspects of copying items (be they files, dirs, or symlinks). The default value for all these optional arguments should be sane (to follow The Principle of Least Astonishment).
Motivation
The need for copying symlinks
Currently, copyDir
do not copy symlinks (see copyDir
implementation, case is not exhaustive). This causes problems. For example, I'm developing a wrapper around Guix. Guix comes in a binary form, packed into the archive. I use nimarchive library. It extracts the archive into a temporary directory first, and only then it moves files into the required destination. moveDir
fallback to copyDir
and this results in a broken Guix distribution, as Guix heavily relies on symlinks.
The need for specifying copying options
Sometimes there is a need to resolve symlinks and copy files/dirs themselves instead of copying symlinks. Examples include copying dir with symlinks (that point to the files/dirs that are not copied) to another computer.
Sometimes there is a need to create hardlinks instead of actual files. Examples include creating a version control system (see Mercurial wiki).
Another problem that arises with copying is "How to handle conflicts?" (in other words, what to do when the file already exists in destination dir). Options include: report an error, keep the existing file, without reporting an error, replace the existing file, or replace the existing file only if it is older than the file being copied.
Description
There was already an attempt to change os module copy/move functions, see nim-lang/Nim#16709.
For the summary on how do different programming languages handle symlinks on dir copy, see nim-lang/Nim#16709 (comment). I think we can copy good design from C++ filesystem::copy
(see copy_options) and from Go copy library.
I propose to define copyFile
, copyDir
as follows:
type SymlinkAction* = enum ## Action to perform on symlink while copying.
saFollow, ## Copy the files symlinks point to
saCopyAsIs, ## Copy symlinks as symlinks
saSkip ## Ignore symlinks
type ExistingFileAction* = enum ## Action to perform on existing file while copying.
efaError, ## Report an error
efaSkipExisting, ## Keep the existing file, without reporting an error
efaOverwriteExisting, ## Replace the existing file
efaUpdateExisting ## Replace the existing file only if it is older than the file being copied
type CopyItemOptions* = object ## Options controlling the files and symlinks copying.
existingFileAction*: ExistingFileAction
recursive*: bool ## Recursively copy subdirectories and their content, used only for dir-like items
symlinkAction*: SymlinkAction
# Could be expanded in the future
proc copyFile*(source, dest: string, options = CopyItemOptions(existingFileAction: efaError, recursive: true, symlinkAction: saFollow)) =
... # Implentation is omitted, see previous attempt: https://github.com/nim-lang/Nim/pull/16709
type CopyOptionsCallback* = proc(source, dest: string, path: string, kind: PathComponent): CopyItemOptions {.closure.}
type CopyFunction* = proc(source, dest: string, path: string, kind: PathComponent, options: CopyOptions, itemOptions: CopyItemOptions) {.closure.}
type CopyOptions* = object ## Options controlling the dirs copying.
copyOptionsCallback*: CopyOptionsCallback
copyFunction*: CopyFunction
# Could be expanded in the future
proc copyOptionsCallbackDefault*(source, dest: string, path: string, kind: PathComponent): CopyItemOptions {.closure.} =
CopyItemOptions(existingFileAction: efaError, recursive: true, symlinkAction: saCopyAsIs)
proc copyFunctionDefault*(source, dest: string, path: string, kind: PathComponent, options: CopyOptions, itemOptions: CopyItemOptions) {.closure.} =
if kind == pcDir:
copyDir(source = source / path, dest = dest / path, options = options)
else:
copyFile(source = source / path, dest = dest / path, options = itemOptions)
proc copyDir*(source, dest: string, options = CopyOptions(copyOptionsCallback: copyOptionsCallbackDefault, copyFunction: copyFunctionDefault)) =
createDir(dest)
for kind, path in walkDir(source):
var noSource = splitPath(path).tail
let itemOptions = options.copyOptionsCallback(source = source, dest = dest, path = path, kind = kind)
if kind == pcLinkToDir and itemOptions.symlinkAction == saFollow:
let kind = pcDir
if kind == pcDir and not itemOptions.recursive:
createDir(dest / noSource)
else:
options.copyFunction(source = source, dest = dest, path = noSource, kind = kind, options = options, itemOptions = itemOptions)
Examples
Before
# 1
[copying symlinks as symlinks was not possible, should call createSymlink]
# 2
[not possible without manual loop with `walkDir`]
# 3
[not possible without manual loop with `walkDir`]
After
# 1
copyFile("/usr/bin/vi", "/home/rominf/bin/vi", CopyItemOptions(symlinkAction: saCopyAsIs))
# 2
proc createHardlinks(source, dest: string, path: string, kind: PathComponent, options: CopyOptions, itemOptions: CopyItemOptions) {.closure.} =
if kind == pcFile:
createHardlink(source / path, dest / path)
else:
options.copyFunction(source = source, dest = dest, path = path, kind = kind, options = options, itemOptions = itemOptions)
copyDir("a", "b", CopyOptions(copyOptionsCallback: copyOptionsCallbackDefault, copyFunction: createHardlinks))
# 3
proc updatePhotos(source, dest: string, path: string, kind: PathComponent): CopyItemOptions {.closure.} =
result = CopyItemOptions(existingFileAction: efaError, recursive: true, symlinkAction: saCopyAsIs)
if kind == pcFile and path.lastPathPart.endsWith(".jpg"):
result.existingFileAction = efaUpdateExisting
copyDir("data", "dataArchive", CopyOptions(copyOptionsCallback: updatePhotos, copyFunction: copyFunctionDefault))
Backward incompatibility
This RFC changes the default behavior of copy functions: symlinks are copied now. In my opinion, we can assume that not copying them was a bug, so no additional actions are required.