Open Source im professionellen Einsatz

© Nolte Lourens, Fotolia.com

Eigenes Userspace-Dateisystem mit Python

Mein eigenes Album

Dateisysteme sind vielen Anwendern und Entwicklern fern - sie sind Angebote, die der Kernel bereitstellt. Dabei lassen sich ohne viel Aufwand mit Fuse und Python anwendungsspezifische Sichten auf den eigenen Datenbestand definieren - etwa ein Fotoalbum.

Viel Auswahl bieten Distributionen bei den Applikationen, auch für außergewöhnliche Wünsche gibt es oft Programme. Um Ordnung in Datenbestände zu bringen, gibt es weniger Alternativen. Filesysteme wie Ext, XFS oder Reiser speichern zwar Dateien, lassen aber wenig Gestaltungsspielraum für eigene Anpassungen. Wer etwa seine digitalen Fotos in komplexen Pfaden verwaltet, wird des Navigierens durch Verzeichnisstrukturen schnell überdrüssig. Dem Folder »images« wäre mit einem Bildbetrachter beispielsweise schwer beizukommen:

images/stuff
images/stuff/homerlinux.gif
images/stuff/pie_chart.gif
images/nature
images/nature/frogs.jpg
images/nature/termite.jpeg

Bei solchen Spezialfällen, etwa um so eine Struktur zu glätten, hilft Fuse weiter, das "Filesystem in Userspace" [1]. Es erlaubt Anwendern über ein einfaches API in das Dateisystem einzugreifen. Weil dafür auch Bindings für Python verfügbar sind, steht dem »AlbumFS« nichts im Wege. Experimentierfreudige passen es ihren Wünschen an.

Verzeichnisse flachklopfen

Python implementiert ein maßgeschneidertes Dateisystem und erlaubt so eine flache Sicht auf verschachtelte Ordnerstrukturen (siehe Abbildung 1). Dazu erzeugt es dynamisch eine Albumseite in HTML (siehe Abbildung 2). Mit

mkdir /tmp/album
python AlbumFS.py -f images /tmp/album

setzt sich das per Skript implementierte Fuse auf das Directory »images«. Das klappt sogar mit normalen Benutzerrechten. Ergebnis ist das Pseudoverzeichnis »/tmp/album«, das »mount« nun anzeigt:

AlbumFS.py on /tmp/album type fuse.AlbumFS.py (rw,nosuid,nodev,user=wr)

Anschließend demontiert »fusermount -u /tmp/album/« das Dateisystems. Damit dies funktioniert, benötigt der Programmierer die Pakete »libfuse2«, »python-fuse« und »Python«. Alle sind in aktuellen Debian- oder Ubuntu-Repositories enthalten.

Abbildung 1: Das Python-Dateisystem bildet alle Bilder der ursprünglichen Verzeichnisstruktur flach ab. Die ehemaligen Pfade kodiert es im Namen.

Abbildung 1: Das Python-Dateisystem bildet alle Bilder der ursprünglichen Verzeichnisstruktur flach ab. Die ehemaligen Pfade kodiert es im Namen.

Abbildung 2: Die HTML-Datei, die ein einfaches Album implementiert, liegt nicht als echte Datei im Zielbereich. Das Python-Fuse legt sie virtuell an.

Abbildung 2: Die HTML-Datei, die ein einfaches Album implementiert, liegt nicht als echte Datei im Zielbereich. Das Python-Fuse legt sie virtuell an.

Die Python-Bindings erledigen das Vorhaben in rund 100 Zeilen lehrreichem Python-Code, siehe Listing 1. Zunächst leitet der Entwickler ab Zeile 17 »AlbumFS« von »fuse.Fuse« ab [2]. Er überschreibt die wichtigsten Methoden, die Dateisysteme implementieren. Das sind beispielsweise »mkdir()«, »link()«, »chmod()« oder »read()«. Da die Klasse »AlbumFS« nur lesend zugreift, beschränkt sich das Beispiel auf die Hilfsoperationen »getattr()«, »readdir()«, »statfs()« und »fsdestroy()« in den Zeilen 27 bis 49. Zusätzlich implementiert der Python-Coder eine "File like"-Klasse »AlbumFSFile« ab Zeile 102. Fuse benutzt sie dann, um die Dateien in dem übergebenen Pseudoverzeichnis auszuliefern.

Entwickler müssen sich Gedanken darüber machen, wie sie die Hierarchie unter »images« flachklopfen. Die beiden Dateien »images/a/x.jpg« und »images/b/x.jpg« sollten in der neuen Verzeichnisstruktur unterscheidbar sein. Dazu konstruiert das Skript aus dem Pfad der Ursprungsdatei den Namen des Ziels. So wird aus »images/a/x.jpg« in der neuen Ansicht »/tmp/album/images_a_x.jpg«. Das erledigt die Methode »_reset_mapping()« ab Zeile 51.

Listing 1:
»AlbumFS.py«

001 #!/usr/bin/env python
002 import os, tempfile, sys, shutil, errno, fuse
003 from PIL import Image
004 fuse.fuse_python_api = (0, 2)
005 
006 IMG_SUFFIX = [".png", ".jpg", ".jpeg", ".gif"]
007 flat2real = {}
008 join = os.path.join
009 
010 def get_real_path(path):
011     real_path, flat_file = os.path.split(path)
012     if path != "/":
013         try: real_path = join(real_path, flat2real[flat_file])
014         except: return None
015     return real_path
016 
017 class AlbumFS(fuse.Fuse):
018     def __init__(self, path, *args, **kw):
019         fuse.Fuse.__init__(self, *args, **kw)
020         self.root = path
021         self.file_class = AlbumFSFile
022         os.chdir(self.root)
023         self.parse(values = self, errex = 1)
024         self.thumb_dir = tempfile.mkdtemp()
025         self.statfs()
026 
027     def getattr(self, path):
028         real_path = get_real_path(path)
029         if real_path:
030             return os.lstat(real_path)
031 
032     def readdir(self, path, offset):
033         for target_name in flat2real.iterkeys():
034             yield fuse.Direntry(target_name)
035 
036     def statfs(self):
037         self._reset_mapping()
038         self._gen_album_html()
039         return os.statvfs(".")
040 
041     def fsdestroy(self): shutil.rmtree(self.thumb_dir)
042 
043     def _create_thumb(self, unique):
044         fn, ext = os.path.splitext(unique)
045         im = Image.open(flat2real[unique])
046         im.thumbnail((128, 128), Image.ANTIALIAS)
047         thumb_fn = join(self.thumb_dir, fn + "_thumbnail" + ext)
048         im.save(thumb_fn)
049         self.flat2thumb[unique] = thumb_fn
050 
051     def _reset_mapping(self):
052         global flat2real
053         flat2real = {}
054         self.flat2thumb = {}
055         for parent, dirs, files in os.walk("."):
056             for fn in files:
057                 if not (os.path.splitext(fn)[1] in IMG_SUFFIX 
058                         and os.path.isfile(join(parent, fn))):
059                     continue
060                 elements = parent.split("/")
061                 if elements[0] == ".": elements = elements[1:]
062                 parent = "_".join(elements)
063                 if parent:
064                     unique = parent + "_" + fn
065                     flat2real[unique] = join(self.root, parent, fn)
066                 else:
067                     unique = fn
068                     flat2real[unique] = join(self.root, fn)
069                 self._create_thumb(unique)
070         flat2real["album.html"] = join(self.thumb_dir,
071                                        "album.html")
072 
073     def _gen_album_html(self):
074         html_content = ['<div align="center"><h4>Album %s</h4>'
075                         '<table cellspacing="10">'
076                         '<tbody align="center" valign="bottom"'
077                         'style="font-size:4px"><tr>'%self.root]
078         max_items = len(self.flat2thumb)
079         for idx, (unique, thumb_fn) in enumerate(
080             self.flat2thumb.items()):
081             img_info = Image.open(flat2real[unique]).size
082             row_content = '<td><a href="file://%s" title="%s">'%
083                           (flat2real[unique], unique) + 
084                           '<img src="%s" alt=""><br>%s</a><br>'%
085                           (thumb_fn, unique) + 
086                           'Size: %s</td>'%str(img_info)
087             if 0 < idx < max_items and idx % 3 == 0:
088                 row_content = "</tr><tr>" + row_content
089             html_content.append(row_content)
090         html_content.append("</tr></tbody></table></div><body>")
091         f = file(join(self.thumb_dir, "album.html"), "w")
092         f.writelines(html_content)
093         f.close()
094 
095 def flag2mode(flags):
096     md = {os.O_RDONLY: 'r', os.O_WRONLY: 'w', os.O_RDWR: 'w+'}
097     m = md[flags & (os.O_RDONLY | os.O_WRONLY | os.O_RDWR)]
098     if flags | os.O_APPEND:
099         m = m.replace('w', 'a', 1)
100     return m
101 
102 class AlbumFSFile(object):
103     def __init__(self, path, flags, *mode):
104         self.file = os.fdopen(os.open(get_real_path(path),
105                                       flags, *mode),
106                               flag2mode(flags))
107         self.fd = self.file.fileno()
108 
109     def read(self, length, offset):
110         self.file.seek(offset)
111         return self.file.read(length)
112 
113     def release(self, flags):
114         self.file.close()
115 
116 if __name__ == '__main__':
117     server = AlbumFS(sys.argv[2])
118     server.main()

Nachrichten aus dem Kern

Entwicklern stellt sich die Frage, wann sie diese Methode ausführen. Rufen sie sie im Konstruktor auf, müssten sie bei jeder Änderung der Verzeichnisstruktur von »images« das Zielverzeichnis neu mounten. Fuse bietet hier eine elegantere Möglichkeit: Es führt regelmäßig die Methode »statfs()« aus, um die Attribute »f_bsize« und »f_frsize« der Struktur »statvfs« abzufragen. Überschreibt der Entwickler diese Methode in »AlbumFS«, hält er die Pseudoverzeichnis-Struktur und die dynamische HTML-Seite aktuell.

Elegante Erweiterungen würden »pyinotify« einsetzen: Dieses Paket benutzt das Feature »inotify()« des Kernels ab Version 2.6.13, um sich direkt vom Betriebsystem über Änderungen im Dateisystem benachrichtigen zu lassen.

Diesen Artikel als PDF kaufen

Express-Kauf als PDF

Umfang: 3 Heftseiten

Preis € 0,99
(inkl. 19% MwSt.)

Als digitales Abo

Als PDF im Abo bestellen

comments powered by Disqus

Ausgabe 07/2013

Preis € 6,40

Insecurity Bulletin

Insecurity Bulletin

Im Insecurity Bulletin widmet sich Mark Vogelsberger aktuellen Sicherheitslücken sowie Hintergründen und Security-Grundlagen. mehr...

Linux-Magazin auf Facebook