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