Copying a symbolic link in Python
Asked Answered
C

2

34

I want to copy a file src to the destination dst, but if src happens to be a symbolic link, preserve the link instead of copying the contents of the file. After the copy is performed, os.readlink should return the same for both src and dst.

The module shutil has several functions, such as copyfile, copy and copy2, but all of these will copy the contents of the file, and will not preserve the link. shutil.move has the correct behavior, other than the fact it removes the original file.

Is there a built-in way in Python to perform a file copy while preserving symlinks?

Crowe answered 31/1, 2011 at 4:24 Comment(0)
E
13

Python 3 follow_symlinks

In Python 3, most copy methods of shutil have learned the follow_symlinks argument, which preserves symlinks if selected.

E.g. for shutil.copy:

shutil.copy(src, dest, follow_symlinks=False)

and the docs say:

shutil.copy(src, dst, *, follow_symlinks=True)

Copies the file src to the file or directory dst. src and dst should be strings. If dst specifies a directory, the file will be copied into dst using the base filename from src. Returns the path to the newly created file.

If follow_symlinks is false, and src is a symbolic link, dst will be created as a symbolic link. If follow_symlinks` is true and src is a symbolic link, dst will be a copy of the file src refers to.

This has one problem however: if you try to overwrite an existing file or symlink, it fails with:

FileExistsError: [Errno 17] File exists: 'b' -> 'c'

unlike the follow_symlinks=True which successfully overwrites.

The same also happens for os.symlink, so I ended up using instead:

#!/usr/bin/env python3

import shutil
import os

def copy(src, dst):
    if os.path.islink(src):
        if os.path.lexists(dst):
            os.unlink(dst)
        linkto = os.readlink(src)
        os.symlink(linkto, dst)
    else:
        shutil.copy(src, dst)

if __name__ == '__main__':
    os.symlink('c', 'b')
    os.symlink('b', 'a')
    copy('a', 'b')

    with open('c', 'w') as f:
        f.write('a')
    with open('d', 'w'):
        pass
    copy('c', 'd')
    copy('a', 'c')

Tested in Ubuntu 18.10, Python 3.6.7.

Enlightenment answered 25/3, 2019 at 8:17 Comment(0)
F
49

Just do

def copy(src, dst):
    if os.path.islink(src):
        linkto = os.readlink(src)
        os.symlink(linkto, dst)
    else:
        shutil.copy(src,dst)

shutil.copytree does something similar, but as senderle noted, it's picky about copying only directories, not single files.

Frumentaceous answered 31/1, 2011 at 4:33 Comment(4)
I tried using copytree to do this and I got an OSError complaining that src wasn't a directory; I think checking os.path.islink really is the only way.Fructose
It think it's weird there's no dedicated function for copying files (rather than directories like copytree does) that preserves symlinks rather than resolving them, but I couldn't find any either. It seems like this is the only way...Jone
Note that in python 3, an additional keyword follow_symlinks=True|False was added to most of the shutil functions. Your answer works for creating the link, but also keeping the metadata could be nice. shutil.copystat() also tries to resolve the source if it's a symlink by default (and borks with ENOENT on broken links), but I'm not sure about the concise way to copy that metadata for symlinks in python2.Theone
It is very important to call shutil.copytree(src, dst, symlinks=True) if you want symlinks to be preserved in the destination dir.Andalusia
E
13

Python 3 follow_symlinks

In Python 3, most copy methods of shutil have learned the follow_symlinks argument, which preserves symlinks if selected.

E.g. for shutil.copy:

shutil.copy(src, dest, follow_symlinks=False)

and the docs say:

shutil.copy(src, dst, *, follow_symlinks=True)

Copies the file src to the file or directory dst. src and dst should be strings. If dst specifies a directory, the file will be copied into dst using the base filename from src. Returns the path to the newly created file.

If follow_symlinks is false, and src is a symbolic link, dst will be created as a symbolic link. If follow_symlinks` is true and src is a symbolic link, dst will be a copy of the file src refers to.

This has one problem however: if you try to overwrite an existing file or symlink, it fails with:

FileExistsError: [Errno 17] File exists: 'b' -> 'c'

unlike the follow_symlinks=True which successfully overwrites.

The same also happens for os.symlink, so I ended up using instead:

#!/usr/bin/env python3

import shutil
import os

def copy(src, dst):
    if os.path.islink(src):
        if os.path.lexists(dst):
            os.unlink(dst)
        linkto = os.readlink(src)
        os.symlink(linkto, dst)
    else:
        shutil.copy(src, dst)

if __name__ == '__main__':
    os.symlink('c', 'b')
    os.symlink('b', 'a')
    copy('a', 'b')

    with open('c', 'w') as f:
        f.write('a')
    with open('d', 'w'):
        pass
    copy('c', 'd')
    copy('a', 'c')

Tested in Ubuntu 18.10, Python 3.6.7.

Enlightenment answered 25/3, 2019 at 8:17 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.