2008-08-21 6 views
16

Wenn ich os.stat() auf einem defekten symlink aufrufen, löst Python eine OSError Ausnahme. Dies macht es nützlich, sie zu finden. Es gibt jedoch einige andere Gründe, warum os.stat() eine ähnliche Ausnahme auslösen könnte. Gibt es einen genaueren Weg, um mit Python unter Linux die defekte symlinks zu erkennen?Finden Sie gebrochene Symlinks mit Python

Antwort

20

Ein gemeinsamer Python sagen ist, dass es einfacher ist, um Vergebung zu bitten, als Genehmigung. Obwohl ich im wirklichen Leben kein Fan von dieser Aussage bin, trifft es in vielen Fällen zu. Normalerweise möchten Sie Code vermeiden, der zwei Systemaufrufe für dieselbe Datei verknüpft, weil Sie nie wissen, was zwischen Ihren beiden Aufrufen in Ihrem Code passieren wird.

Ein typischer Fehler ist so etwas wie zu schreiben:

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

Der zweite Aufruf (os.unlink) kann fehlschlagen, wenn etwas anderes es nach dem if-Test gelöscht, eine Ausnahme auslösen, und den Rest stoppen Ihrer Funktion ausführen.(Du denkst vielleicht, dass das im wirklichen Leben nicht passiert, aber wir haben letzte Woche einen anderen Bug aus unserer Codebase gefischt - und es war die Art von Bug, der ein paar Programmierer am Kopf kratzte und 'Heisenbug' für die letzten Monate)

in Ihrem speziellen Fall also würde ich wahrscheinlich tun:

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 

Der Ärger ist hier, dass stat die gleiche Fehlercode für einen Symlink zurückgibt, die einfach nicht da ist und ein gebrochenes Symlink.

Also, ich denke, Sie haben keine andere Wahl als die Unteilbarkeit zu brechen, und so etwas wie

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

Readlink auch errno == ENOTDIR wenn der Symlink missuses eine Datei als Verzeichnis festlegen. –

+3

os.readlink (Pfad) erhält möglicherweise nicht den tatsächlichen Pfad, wenn dem Pfad 'link' ein relativer Pfad zu seinem Ziel zugewiesen wird. Wenn Pfad beispielsweise mit '../target' verknüpft ist, gibt os.path.exists (os.readlink (Pfad)) false zurück, weil Sie den Pfad nicht im Pfad angeben, in dem sich die Verknüpfung befindet Ihr oberes Verzeichnis hat keine Datei oder keinen Ordner namens 'target'. Eine sichere Möglichkeit, dies zu vermeiden, ist os.path.exists (os.path.realpath (path)). – AplusG

+0

Auch das ist nicht gut genug. realpath interpretiert den Pfad des Symlinks relativ zum aktuellen laufenden Verzeichnis des aktuell laufenden Skripts, während ein symbolischer Link vom Betriebssystem relativ zu dem Ordner interpretiert wird, in dem sich der symbolische Link befindet. Was Sie tun müssen, ist etwas wie folgt zu tun: link_target = os.readlink (Pfad) dir = os.path.dirname (Pfad) wenn nicht os.path.isabs (link_target): link_target = os.path.join (dir, link_target) wenn os.path.exists (link_target): # tun, was Sie mit diesem schlechten symlink –

3

Kann ich das Testen für Hardlinks ohne Python erwähnen?/bin/test hat die Bedingung FILE1 -ef FILE2, die erfüllt ist, wenn Dateien einen Inode teilen.

Daher funktioniert etwas wie find . -type f -exec test \{} -ef /path/to/file \; -print für harte Verbindungstests zu einer bestimmten Datei.

Das bringt mich zu man test Lesen und die Erwähnungen von -L und -h, die beide arbeiten auf eine Datei und true zurück, wenn die Datei ein symbolischer Link ist, aber die Sie nicht sagen, ob das Ziel fehlt.

Ich fand, dass head -0 FILE1 würde einen Exit-Code von 0 zurück, wenn die Datei geöffnet werden kann und ein 1 wenn es nicht kann, was im Fall eines symbolischen Link zu einer normalen Datei als Test arbeitet dafür, ob Ziel es ist möglich gelesen werden.

1

Ich bin kein Python-Typ, aber es sieht aus wie os.readlink()? Die Logik, die ich in Perl verwenden würde, ist die Verwendung von readlink(), um das Ziel zu finden, und die Verwendung von stat(), um zu testen, ob das Ziel existiert.

Edit: Ich schlug einige Perl, die Demos Readlink. Ich glaube, die Perl-Stat und Readlink und Pythons os.stat() und os.readlink() sind beide Wrapper für die Systemaufrufe, so sollte dies vernünftig übersetzen und Proof of Concept-Code:

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 > 
11

os.lstat() kann hilfreich sein . Wenn lstat() erfolgreich ist und stat() fehlschlägt, handelt es sich wahrscheinlich um einen fehlerhaften Link.

2

os.path

Sie können versuchen, mit realpath() zu bekommen, was der Symlink auf, dann zu bestimmen versuchen herauszufinden, ob es eine gültige Datei ist, ist die Verwendung der Datei.

(Ich bin nicht in der Lage, dass in dem Moment zu versuchen, so dass Sie mit ihm spielen, um müssen und sehen, was Sie erhalten)

8

Das ist nicht atomar ist, aber es funktioniert.

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

Tat von RTFM (die fantastische Handbuch zu lesen) sehen wir

os.path.exists (Pfad)

Return True, wenn Pfad zu einem vorhandenen Pfad verweist. Gibt für fehlerhafte symbolische Links False zurück.

Er sagt auch:

Auf einigen Plattformen diese Funktion Falsch zurückgeben kann, wenn die Erlaubnis nicht erteilt wird os.stat() auf der gewünschten Datei auszuführen, auch wenn der Pfad vorhanden ist physisch.

Wenn Sie sich also Sorgen um Berechtigungen machen, sollten Sie weitere Klauseln hinzufügen.

+0

+1 für 'os.path.islink (Dateiname) und nicht os.path.exists (Dateiname)' was mir geholfen hat, und dafür, dass das F in RTFM Fantastic ist. – esmit

0

Ich hatte ein ähnliches Problem: Wie man gebrochene symbolische Links fängt, selbst wenn sie in einem Elternteil vorkommen? Ich wollte auch alle von ihnen (in einer Anwendung, die sich mit einer ziemlich großen Anzahl von Dateien beschäftigt) protokollieren, aber ohne zu viele Wiederholungen.

Hier ist, was ich, einschließlich Komponententests, kam.

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) 

Unit-Tests:

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()