Pergunta

Se eu ligar os.stat() em um quebrado symlink, python lança um OSError exceção.Isso o torna útil para encontrá-los.No entanto, existem algumas outras razões que os.stat() pode lançar uma exceção semelhante.Existe uma maneira mais precisa de detectar falhas symlinks com Python no Linux?

Foi útil?

Solução

Um ditado comum em Python é que é mais fácil pedir perdão do que permissão.Embora eu não seja fã dessa afirmação na vida real, ela se aplica em muitos casos.Normalmente você deseja evitar código que encadeia duas chamadas de sistema no mesmo arquivo, porque você nunca sabe o que acontecerá com o arquivo entre as duas chamadas em seu código.

Um erro típico é escrever algo como:

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

A segunda chamada (os.unlink) pode falhar se alguma outra coisa a excluir após o teste if, gerar uma exceção e interromper a execução do restante da sua função.(Você pode pensar que isso não acontece na vida real, mas acabamos de pescar outro bug como esse em nossa base de código na semana passada - e foi o tipo de bug que deixou alguns programadores coçando a cabeça e alegando 'Heisenbug' para o últimos meses)

Então, no seu caso específico, eu provavelmente faria:

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

O incômodo aqui é que stat retorna o mesmo código de erro para um link simbólico que simplesmente não existe e um link simbólico quebrado.

Então, acho que você não tem escolha a não ser quebrar a atomicidade e fazer algo como

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

Outras dicas

os.lstat() pode ser útil.Se lstat() for bem-sucedido e stat() falhar, provavelmente é um link quebrado.

Isso não é atômico, mas funciona.

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

Na verdade por RTFM(lendo o fantástico manual) vemos

os.path.exists(caminho)

Retorna True se path se referir a um caminho existente.Retorna False para links simbólicos quebrados.

Também diz:

Em algumas plataformas, esta função pode retornar False se não for concedida permissão para executar os.stat() no arquivo solicitado, mesmo que o caminho exista fisicamente.

Portanto, se você está preocupado com permissões, adicione outras cláusulas.

Posso mencionar o teste de hardlinks sem python?/bin/test possui a condição FILE1 -ef FILE2 que é verdadeira quando os arquivos compartilham um inode.

Portanto, algo como find . -type f -exec test \{} -ef /path/to/file \; -print funciona para testes de link físico para um arquivo específico.

O que me leva à leitura man test e as menções de -L e -h que funcionam em um arquivo e retornam verdadeiros se esse arquivo for um link simbólico, mas isso não informa se o destino está faltando.

eu achei isso head -0 FILE1 retornaria um código de saída de 0 se o arquivo puder ser aberto e um 1 se não puder, o que no caso de um link simbólico para um arquivo normal funciona como um teste para saber se o destino pode ser lido.

os.path

Você pode tentar usar realpath() para obter o que o link simbólico aponta e, em seguida, tentar determinar se é um arquivo válido usando o arquivo.

(Não posso experimentar isso no momento, então você terá que brincar com isso e ver o que consegue)

Não sou um cara de python, mas parece os.readlink()?A lógica que eu usaria em perl é usar readlink() para encontrar o alvo e usar stat() para testar se o alvo existe.

Editar:Eu digitei um pouco de perl que demonstra o readlink.Acredito que stat e readlink do perl e os.stat() e os.readlink() do python são ambos wrappers para as chamadas do sistema, então isso deve traduzir razoavelmente bem como código de prova de conceito:

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 >

Eu tive um problema parecido:como capturar links simbólicos quebrados, mesmo quando eles ocorrem em algum diretório pai?Eu também queria registrar todos eles (em um aplicativo que lida com um número bastante grande de arquivos), mas sem muitas repetições.

Aqui está o que eu criei, incluindo testes de unidade.

arquivoutil.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)

Testes unitários:

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()
Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top