Подтвердить что ты не робот

Найти сломанные символические ссылки с помощью Python

Если я вызываю os.stat() на сломанном symlink, python генерирует исключение OSError. Это делает его полезным для их поиска. Однако есть еще несколько причин, по которым os.stat() может вызвать аналогичное исключение. Есть ли более точный способ обнаружения сломанного symlinks с Python под Linux?

4b9b3361

Ответ 1

Общепринятая фраза Python заключается в том, что проще просить прощения, чем разрешения. Хотя я не являюсь поклонником этого заявления в реальной жизни, он применяется во многих случаях. Обычно вы хотите избежать кода, который объединяет два системных вызова в один и тот же файл, потому что вы никогда не знаете, что произойдет с файлом между вашими двумя вызовами в вашем коде.

Типичная ошибка заключается в том, чтобы написать что-то вроде:

if os.path.exists(path):
    os.unlink(path)

Второй вызов (os.unlink) может завершиться неудачно, если что-то еще удалило его после теста if, поднимет Exception и остановит остальную часть вашей функции. (Возможно, вы думаете, что этого не происходит в реальной жизни, но на прошлой неделе мы просто выудили еще одну ошибку из нашей кодовой базы - и это была ошибка, из-за которой несколько программистов почесывали голову и требовали "Гейзенбуга" для последние несколько месяцев)

Итак, в вашем конкретном случае, я бы, вероятно, сделал:

try:
    os.stat(path)
except OSError, e:
    if e.errno == errno.ENOENT:
        print 'path %s does not exist or is a broken symlink' % path
    else:
        raise e

Досада здесь заключается в том, что stat возвращает тот же код ошибки для символической ссылки, которой просто нет, и сломанной символической ссылки.

Итак, я думаю, у вас нет выбора, кроме как разбить атомарность и сделать что-то вроде

if not os.path.exists(os.readlink(path)):
    print 'path %s is a broken symlink' % path

Ответ 2

os.lstat() может оказаться полезным. Если lstat() преуспевает, а stat() терпит неудачу, то это, вероятно, неработающая ссылка.

Ответ 3

Это не атомный, но он работает.

os.path.islink(filename) and not os.path.exists(filename)

Действительно RTFM  (читая фантастическое руководство), мы видим

os.path.exists(путь)

Возвращает значение True, если путь относится к существующему пути. Возвращает False для сломанных символических ссылок.

В нем также говорится:

На некоторых платформах эта функция может возвращать False, если разрешение не предоставлено для выполнения os.stat() в запрошенном файле, даже если путь физически существует.

Итак, если вас беспокоят разрешения, вы должны добавить другие предложения.

Ответ 4

Можно ли упоминать тестирование жестких ссылок без python?/bin/test имеет условие FILE1 -ef FILE2, которое истинно, когда файлы совместно используют inode.

Следовательно, что-то вроде find . -type f -exec test \{} -ef /path/to/file \; -print работает для жесткого тестирования ссылок на определенный файл.

Это приводит меня к чтению man test и упоминаниям -L и -h, которые работают в одном файле и возвращают true, если этот файл является символической ссылкой, однако это не говорит вам, отсутствует.

Я обнаружил, что head -0 FILE1 вернет код выхода 0, если файл можно открыть, а 1, если он не может, который в случае символической ссылки на обычный файл работает как тест для того, можно ли его читать.

Ответ 5

os.path

Вы можете попытаться использовать realpath(), чтобы получить то, на что указывает симлинк, а затем попытаться определить, является ли он допустимым файлом с файлом.

(Я не могу попробовать это сейчас, поэтому вам придется поиграть с ним и посмотреть, что вы получите)

Ответ 6

Я не парень python, но он выглядит как os.readlink()? Логикой, которую я использовал бы в perl, является использование readlink() для поиска цели и использования stat() для проверки, существует ли цель.

Изменить: я удалил некоторые perl, что demos readlink. Я считаю, что perl stat и readlink и python os.stat() и os.readlink() являются обертками для системных вызовов, поэтому это должно хорошо понимать как доказательство кода концепции:

wembley 0 /home/jj33/swap > cat p
my $f = shift;

while (my $l = readlink($f)) {
  print "$f -> $l\n";
  $f = $l;
}

if (!-e $f) {
  print "$f doesn't exist\n";
}
wembley 0 /home/jj33/swap > ls -l | grep ^l
lrwxrwxrwx    1 jj33  users          17 Aug 21 14:30 link -> non-existant-file
lrwxrwxrwx    1 root     users          31 Oct 10  2007 mm -> ../systems/mm/20071009-rewrite//
lrwxrwxrwx    1 jj33  users           2 Aug 21 14:34 mmm -> mm/
wembley 0 /home/jj33/swap > perl p mm
mm -> ../systems/mm/20071009-rewrite/
wembley 0 /home/jj33/swap > perl p mmm
mmm -> mm
mm -> ../systems/mm/20071009-rewrite/
wembley 0 /home/jj33/swap > perl p link
link -> non-existant-file
non-existant-file doesn't exist
wembley 0 /home/jj33/swap >

Ответ 7

У меня была аналогичная проблема: как поймать сломанные символические ссылки, даже если они встречаются в каком-то родительском каталоге? Я также хотел зарегистрировать их все (в приложении, имеющем довольно большое количество файлов), но не слишком много повторений.

Вот что я придумал, включая модульные тесты.

fileutil.py

import os
from functools import lru_cache
import logging

logger = logging.getLogger(__name__)

@lru_cache(maxsize=2000)
def check_broken_link(filename):
    """
    Check for broken symlinks, either at the file level, or in the
    hierarchy of parent dirs.
    If it finds a broken link, an ERROR message is logged.
    The function is cached, so that the same error messages are not repeated.

    Args:
        filename: file to check

    Returns:
        True if the file (or one of its parents) is a broken symlink.
        False otherwise (i.e. either it exists or not, but no element
        on its path is a broken link).

    """
    if os.path.isfile(filename) or os.path.isdir(filename):
        return False
    if os.path.islink(filename):
        # there is a symlink, but it is dead (pointing nowhere)
        link = os.readlink(filename)
        logger.error('broken symlink: {} -> {}'.format(filename, link))
        return True
    # ok, we have either:
    #   1. a filename that simply doesn't exist (but the containing dir
           does exist), or
    #   2. a broken link in some parent dir
    parent = os.path.dirname(filename)
    if parent == filename:
        # reached root
        return False
    return check_broken_link(parent)

Единичные тесты:

import logging
import shutil
import tempfile
import os

import unittest
from ..util import fileutil


class TestFile(unittest.TestCase):

    def _mkdir(self, path, create=True):
        d = os.path.join(self.test_dir, path)
        if create:
            os.makedirs(d, exist_ok=True)
        return d

    def _mkfile(self, path, create=True):
        f = os.path.join(self.test_dir, path)
        if create:
            d = os.path.dirname(f)
            os.makedirs(d, exist_ok=True)
            with open(f, mode='w') as fp:
                fp.write('hello')
        return f

    def _mklink(self, target, path):
        f = os.path.join(self.test_dir, path)
        d = os.path.dirname(f)
        os.makedirs(d, exist_ok=True)
        os.symlink(target, f)
        return f

    def setUp(self):
        # reset the lru_cache of check_broken_link
        fileutil.check_broken_link.cache_clear()

        # create a temporary directory for our tests
        self.test_dir = tempfile.mkdtemp()

        # create a small tree of dirs, files, and symlinks
        self._mkfile('a/b/c/foo.txt')
        self._mklink('b', 'a/x')
        self._mklink('b/c/foo.txt', 'a/f')
        self._mklink('../..', 'a/b/c/y')
        self._mklink('not_exist.txt', 'a/b/c/bad_link.txt')
        bad_path = self._mkfile('a/XXX/c/foo.txt', create=False)
        self._mklink(bad_path, 'a/b/c/bad_path.txt')
        self._mklink('not_a_dir', 'a/bad_dir')

    def tearDown(self):
        # Remove the directory after the test
        shutil.rmtree(self.test_dir)

    def catch_check_broken_link(self, expected_errors, expected_result, path):
        filename = self._mkfile(path, create=False)
        with self.assertLogs(level='ERROR') as cm:
            result = fileutil.check_broken_link(filename)
            logging.critical('nothing')  # trick: emit one extra message, so the with assertLogs block doesn't fail
        error_logs = [r for r in cm.records if r.levelname is 'ERROR']
        actual_errors = len(error_logs)
        self.assertEqual(expected_result, result, msg=path)
        self.assertEqual(expected_errors, actual_errors, msg=path)

    def test_check_broken_link_exists(self):
        self.catch_check_broken_link(0, False, 'a/b/c/foo.txt')
        self.catch_check_broken_link(0, False, 'a/x/c/foo.txt')
        self.catch_check_broken_link(0, False, 'a/f')
        self.catch_check_broken_link(0, False, 'a/b/c/y/b/c/y/b/c/foo.txt')

    def test_check_broken_link_notfound(self):
        self.catch_check_broken_link(0, False, 'a/b/c/not_found.txt')

    def test_check_broken_link_badlink(self):
        self.catch_check_broken_link(1, True, 'a/b/c/bad_link.txt')
        self.catch_check_broken_link(0, True, 'a/b/c/bad_link.txt')

    def test_check_broken_link_badpath(self):
        self.catch_check_broken_link(1, True, 'a/b/c/bad_path.txt')
        self.catch_check_broken_link(0, True, 'a/b/c/bad_path.txt')

    def test_check_broken_link_badparent(self):
        self.catch_check_broken_link(1, True, 'a/bad_dir/c/foo.txt')
        self.catch_check_broken_link(0, True, 'a/bad_dir/c/foo.txt')
        # bad link, but shouldn't log a new error:
        self.catch_check_broken_link(0, True, 'a/bad_dir/c')
        # bad link, but shouldn't log a new error:
        self.catch_check_broken_link(0, True, 'a/bad_dir')

if __name__ == '__main__':
    unittest.main()