Поиск неработающих символических ссылок с помощью Python

StackOverflow https://stackoverflow.com/questions/20794

  •  09-06-2019
  •  | 
  •  

Вопрос

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

Это было полезно?

Решение

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

Типичная ошибка - писать что-то вроде:

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

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

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

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

Другие советы

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

Это не атомарно, но это работает.

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

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

os.path.exists(путь)

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

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

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

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

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

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

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

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

os.путь

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

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

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

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

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 >

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

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

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()
Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top