Skip to content

Add a flexible way to handle individual items while copying with sane defaults (stdlib/os) #319

@rominf

Description

@rominf

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions