Поиск неработающих символических ссылок с помощью Python
Вопрос
Если я позвоню 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
если это невозможно, что в случае символической ссылки на обычный файл работает как проверка того, может ли быть прочитан его целевой объект.
Вы можете попробовать использовать 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()