Modifying a symlink in python

2020-08-09 04:43发布

问题:

How do I change a symlink to point from one file to another in Python?

The os.symlink function only seems to work to create new symlinks.

回答1:

If you need an atomic modification, unlinking won't work.

A better solution would be to create a new temporary symlink, and then rename it over the existing one:

os.symlink(target, tmpLink)
os.rename(tmpLink, linkName)

You can check to make sure it was updated correctly too:

if os.path.realpath(linkName) == target:
    # Symlink was updated

According to the documentation for os.rename though, there may be no way to atomically change a symlink in Windows. In that case, you would just delete and re-create.



回答2:

A little function for Python2 which tries to symlink and if it fails because of an existing file, it removes it and links again. Check Tom Hale's answer for a up-to-date solution.

import os, errno

def symlink_force(target, link_name):
    try:
        os.symlink(target, link_name)
    except OSError, e:
        if e.errno == errno.EEXIST:
            os.remove(link_name)
            os.symlink(target, link_name)
        else:
            raise e


回答3:

You could os.unlink() it first, and then re-create using os.symlink() to point to the new target.



回答4:

I researched this question recently, and found out that the best way is indeed to unlink and then symlink. But if you need just to fix broken links, for example with auto-replace, then you can do os.readlink:

for f in os.listdir(dir):
    path = os.path.join(dir, f)
    old_link = os.readlink(path)
    new_link = old_link.replace(before, after)
    os.unlink(path)
    os.symlink(new_link, path)


回答5:

Given overwrite=True, this function will safely overwrite an existing file with a symlink.

It is cognisant of race conditions, which is why it is not short, but it is safe.

import os, tempfile

def symlink(target, link_name, overwrite=False):
    '''
    Create a symbolic link named link_name pointing to target.
    If link_name exists then FileExistsError is raised, unless overwrite=True.
    When trying to overwrite a directory, IsADirectoryError is raised.
    '''

    if not overwrite:
        os.symlink(target, linkname)
        return

    # os.replace() may fail if files are on different filesystems
    link_dir = os.path.dirname(link_name)

    # Create link to target with temporary filename
    while True:
        temp_link_name = tempfile.mktemp(dir=link_dir)

        # os.* functions mimic as closely as possible system functions
        # The POSIX symlink() returns EEXIST if link_name already exists
        # https://pubs.opengroup.org/onlinepubs/9699919799/functions/symlink.html
        try:
            os.symlink(target, temp_link_name)
            break
        except FileExistsError:
            pass

    # Replace link_name with temp_link_name
    try:
        # Pre-empt os.replace on a directory with a nicer message
        if os.path.isdir(link_name):
            raise IsADirectoryError(f"Cannot symlink over existing directory: '{link_name}'")
        os.replace(temp_link_name, link_name)
    except:
        if os.path.islink(temp_link_name):
            os.remove(temp_link_name)
        raise

Notes for pedants:

  1. If the function fails (e.g. computer crashes), an additional random link to the target might exist.

  2. An unlikely race condition still remains: the symlink created at the randomly-named temp_link_name could be modified by another process before replacing link_name.

I raised a python issue to highlight the issues of os.symlink() requiring the target to not exist, where I was advised to raise my suggestion on the python-ideas mailing list

Credit to Robert Siemer’s input.



回答6:

Don't forget to add a raise command in the case when e.errno != errno.EEXIST You don't want to hide an error then:

if e.errno == errno.EEXIST:
     os.remove(link_name)
     os.symlink(target, link_name)
else:
    raise


回答7:

A quick and easy solution:

while True:
     try:
         os.symlink(target, link_name)
         break
     except FileExistsError:
         os.remove(link_name)

However this has a race condition when replacing a symlink which should always exist, eg:

 /lib/critical.so -> /lib/critical.so.1.2

When upgrading by:

 my_symlink('/lib/critical.so.2.0', '/lib/critical.so')

There is a point in time when /lib/critical.so doesn't exist.

This answer avoids the race condition.