Überschreiben hochgeladener Dateien
Wird eine Datei für ein FileField oder ImageField hochgeladen und exisitiert bereits, überschreibt Django diese Datei nicht sondern hängt einen Tiefstrich an die Datei. Die ältere Datei bleibt ohne Verweis im Dateisystem bestehen.
Überschreibst du also eine Datei rechnung.pdf drei mal, entsteht eine lustige Kaskade im Dateisystem.
- rechnung.pdf
- rechnung_.pdf
- rechnung__.pdf
- rechnung___.pdf (Der Pfad zu dieser Datei wird im FileField gespeichert)
Es ist nicht immer im Interesse, diese alten Dateien zu behalten. Sei es aus Platzmangel, Datenschutzgründen oder weil man einfach nur Ordnung auf seinem System halten will.
Derzeit gibt es keinen direkten Weg, das überschreiben zu erzwingen. Es gibt schon seit längerer Zeit ein Ticket im Django-Repository aber es schaut nicht so aus, als ob es in Django 1.1 eingespielt wird.
Automatisches Übeschreiben mit einem Custom-Storage ###
Hochgeladene Dateien werden von einem eigenen Modul, dem FileSystemStorage verarbeitet. Es öffnet und speichert die Dateien und prüft, ob der Dateiname schon exisitiert und hängt ggf. einen Tiefstrich an die Datei. Das passiert in der Methode get_available_name. Genau diese ist der Ansatzpunkt um das Überschreiben zu erzwingen.
Hier wird ein eigenes Storage erstellt aber vor dem Speichern der Datei, wird (wenn sie existiert) die alte Datei gelöscht:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | from django.db import models from django.core.files.storage import FileSystemStorage class OverwriteFileSystemStorage(FileSystemStorage): def get_available_name(self, name): if self.exists(name): self.delete(name) return name class Invoice(models.Model): title = models.CharField(max_length=120) invoice = models.FileField(upload_to=upload_invoice, storage=OverwriteFileSystemStorage()) def __unicode__(self): return self.title |
Das Custom-Storage wird dem FileField oder dem ImageField als Attribut storage übergeben. Soll dieses Schema für alle Uploads gelten, ändere in der settings.py den Wert DEFAULT_FILE_STORAGE Standardmässig wird das oben genannte FileSystemStorage verwendet.
1 2 3 4 5 | # Original DEFAULT_FILE_STORAGE = 'django.core.files.storage.FileSystemStorage' # Immer alle Dateien überschreiben DEFAULT_FILE_STORAGE = 'path.to.OverwriteFileSystemStorage' |
Den originalen Dateinamen zusätzlich speichern
Dass Dateien nicht grundsätzlich überschrieben werden ist ja grundsätzlich keine schlechte Idee. Schließlich hat man immer ein automatisches Backup aller Änderungen. Lassen wir das Schema so wie es ist aber speichern wir den originalen Dateinamen zusätzlich in einem eigenen Feld. So können wir dem User später den orignalen Dateinamen anzeigen, im Dateisystem bleiben aber alle überschriebenen Dateien erhalten.
Ein upload-Callable ist ein schneller und sicherer Weg, dies zu erreichen. Mehr dazu hatte ich schon in Ordnung im Medienordner geschrieben.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | from django.db import models class Invoice(models.Model): def upload_invoice(instance, filename): instance.invoice_orig_filename = filename return filename title = models.CharField(max_length=120) invoice = models.FileField(upload_to=upload_invoice, storage=OverwriteFileSystemStorage()) invoice_orig_filename = models.CharField(max_length=100, blank=True) def __unicode__(self): return self.title |
Dateien löschen

Der Vollständigkeit halber: Von Haus aus ist es im Admin noch nicht möglich, hochgeladene Dateien wieder zu löschen. Stephan Jäkel hat ein Admin-Widget konstruiert, dass Dateien auf Klick wieder löscht.
Group By Funktionen in Querysets (Django 1.1+)
In meinem aktuellen Projekt will ich die Anzahl neuer Einträge in einem Model pro Tag in einer Grafik darstellen, ich habe also ein Problem wie:
Gib mir eine Liste aller Tage an denen Beiträge hinzugefügt wurden und wie viele Beiträge waren es an diesem Tag.
Ein SQL-Query dafür ist einfach:
1 | SELECT count(id) as counter, created FROM entry_table GROUP BY created |
Nun schauen wir mal, wie sich das mit Django 1.1 Bordmitteln lösen lässt. Wir erstellen ein Django-Model:
1 2 3 4 5 | from django.db import models class Entry(models.Model): created = models.DateField() title = models.CharField(max_length=120) |
Und füttern es gleich mit ein paar Testdaten:
1 2 3 | >>> Entry.objects.create(created='2009-03-25', title='Irgendein Titel') >>> Entry.objects.create(created='2009-03-24', title='Irgendein Titel') >>> Entry.objects.create(created='2009-03-24', title='Irgendein Titel') |
Während man in Django 1.0 noch rohes SQL walten lassen musste, wurden Django 1.1 (aktuell noch django-trunk) einige Aggreagations-Funktionen hinzugefügt, um übliche Aufgaben wie Count, Average, Max und Min etc. direkt im Queryset durchführen zu können. Für unseren Fall brauchen wir die Count-Funktion.
1 2 3 4 | >>> from django.db.models import Count >>> Entry.objects.values('created').annotate(counter=Count('id')) [{'counter': 2, 'created': datetime.date(2009, 3, 24)}, {'counter': 1, 'created': datetime.date(2009, 3, 25)}] |
values('created') triggert hierbei das "GROUP BY created" und Count('id') ist das Pendant zur SQL-Funktion COUNT(id). annotate fügt diese Daten dem SELECT hinzu.
Super einfach, oder? ![]()
Umgang mit DateTime Feldern
Nun ist es aber in der Regel so, dass man einem Artikel kein DateField sondern ein DateTimeField zuweist, also die Zeit mitspeichert:
1 2 3 | class Entry(models.Model): created = models.DateTimeField() title = models.CharField(max_length=120) |
Füttern wir das Model wieder mit ein paar Testdaten:
1 2 3 | >>> Entry.objects.create(created='2009-03-25 12:30:00', title='Irgendein Titel') >>> Entry.objects.create(created='2009-03-24 10:25:00', title='Irgendein Titel') >>> Entry.objects.create(created='2009-03-24 8:56:00', title='Irgendein Titel') |
Und führen unseren obigen Query aus:
1 2 3 4 | >>> Entry.objects.values('created').annotate(counter=Count('id')) [{'counter': 1, 'created': datetime.datetime(2009, 3, 24, 8, 56)}, {'counter': 1, 'created': datetime.datetime(2009, 3, 24, 10, 25)}, {'counter': 1, 'created': datetime.datetime(2009, 3, 25, 12, 30)}] |
Man sieht, das Ergebnis ist unbrauchbar. Logisch, denn es wird nach Datum und Zeit gruppiert. Wir brauchen also nur das Datum aus dem DateTime-Objekt. In SQL gibt es dafür die Funktion DATE(datetime-Feld) die das Datum extrahiert:
1 | SELECT COUNT(id) as counter, DATE(created) as day FROM entry_table GROUP BY day |
Dummerweise liefert Django keinen Filter mit, um eine solche Aufgabe zu lösen. Es gibt zwar die dates Methode, um Zeitperioden aus einem Queryset zu extrahieren, diese liefert aber nur die DateTime-Objekte zurück, kein komplettes Queryset, dass für annotate notwendig ist.
Es muss also doch (etwas) rohes SQL her. Dafür ist die extra Methode gedacht. Mit ihr kann man rohes SQL in den Query einsetzen. Folgendes Beispiel resultiert gleich:
1 2 | Queryset: Entry.objects.extra(select={'day': 'DATE(`created`)'})
SQL: SELECT *, DATE(`created`) AS day FROM entry_table
|
Zusammengefügt schaut unser Queryset jetzt so aus:
1 2 3 4 5 | >>> entries = Entry.objects.extra(select={'day': 'DATE(`created`)'}) .values('day').annotate(counter=Count('id')) >>> entries [{'counter': 2, 'day': u'2009-03-24'}, {'counter': 1, 'day': u'2009-03-25'}] |
Die Tage sind hierbei aber ein unicode-String, ein DateTime-Objekt wäre schöner. Mappen wir den Queryset noch einmal:
1 2 3 4 5 6 | >>> entries = Entry.objects.extra(select={'day': 'DATE(`created`)'}) .values('day').annotate(counter=Count('id')) >>> entries = [{'counter': i['counter'], 'day': datetime.strptime(i['day'], '%Y-%m-%d')} for i in entries] >>> entries [{'counter': 2, 'day': datetime.datetime(2009, 3, 24, 0, 0)}, {'counter': 1, 'day': datetime.datetime(2009, 3, 25, 0, 0)}] |
Da sind sie, die Tage an denen Beiträge hinzugefügt wurden inkl. deren Anzahl. ![]()
War diesmal vielleicht (noch) verworrener als sonst, wer Fragen dazu hat kann mir wie immer mailen oder in die Kommentare schreiben. ![]()
Automatisch lokalisierte Zeitformatierungen
Ein nur knapp dokumentiertes Feature ist die Lokalisierung von String innerhalb von Templatetags und Filtern. Dort reicht es, den betreffenden String in einen gettext-Shortcut zu setzen:
1 | {{ _("hello world")|upper }} |
Dieser Token würde als HELLO WORLD ausgegeben und natürlich auch mit den betreffenden .po Dateien lokalisierbar sein. Richtig sinnvoll wird es beim date Filter. Bisher hast du vielleicht das Datum fest vorgegeben:
1 | {{ entry.published|date:"d.m.Y H:M:S" }} |
Funktioniert im Deutschen ganz gut, aber da ja bekannterweise jede Sprache das Datum und die Zeit irgendwie anders formatiert, ist es eine gute Idee, die Formatierung auch lokalisierbar anzubieten:
1 | {{ entry.published|date:_("DATETIME_FORMAT") }} |
Schaut ein wenig komisch aus, funktioniert aber.
Clevererweise kennt Django bereits den i18n-String DATETIME_FORMAT und liefert für jede mitgelieferte Sprache die entsprechende Formatierung in den gettext-Katalogen mit. Je nach Locale-Einstellung ist die Ausgabe landestypisch:
1 2 3 | de-de: 21. März 2009, 20:24 en-us: March 21, 2009, 8:24 p.m. pt-br: 21 de Março de 2009 às 20:24 |
Neben DATETIME_FORMAT werden auch gleich DATE_FORMAT, TIME_FORMAT, YEAR_MONTH_FORMAT und MONTH_DAY_FORMAT mitgeliefert. ![]()
Sichere Settings in der ServerError Site
Manchmal lässt es sich nicht vermeiden, auf einem Live-System Djangos Fehler (Exception) Seite, den 500-View anzeigen zu lassen. Sie enthält den Traceback sowie den Umgebungsvariablen und allen Settings, Dinge die ein normaler Besucher eigentlich nicht sehen soll.
In den Settings sind schon einige, sicherheitstechnisch riskante Felder gesternt, DB_PASSWORD zum Beispiel. Fast jeder definiert aber eigene Settings, wie ich hier z.B. den Wert TWITTER_USERNAME mit meinem Twitter-Namen.
Dieses Setting ist jetzt zwar nicht hochriskant aber es soll nur als Beispiel dienen. Andere, geheimere Settings wären z.B. dein Flickr-API-Key. Jedenfalls, der zufällige Besucher auf so einer 500er Seite soll diesen Wert niemals sehen.
Eine Möglichkeit ist, den Namen nicht komplett in Großbuchstaben schreiben, twitter_username = 'bartTC' und schon wird er nicht mehr in dieser Liste auftauchen. Das funktioniert zwar wunderbar ... schaut aber komisch aus. Definitionen sollten in Großbuchstaben definiert werden. (Gibts ein PIP dazu?)
Eine bessere Möglichkeit ist es, dem Namen der Funktion eines der Wörter SECRET, PASSWORD oder PROFANITIES_LIST hinzuzufügen.
Ein Wert wie TWITTER_SECRET_USERNAME wird durch das vorhandene SECRET nämlich ausgesternt. ![]()
Weiter lesen
- Eric Holscher hat einen Beitrag geschrieben, der die 500 und 404 Seiten nur Superusern oder Usern mit der IP in
settings.INTERNAL_IPS, anzeigt: Debugging Django in Production Environments
Zugemüllt
Was mach ich am Wochenende?
- Ein Haus bauen.
- Ein Buch schreiben.
- Meinen Schreibtisch aufräumen.
Dürfte alles etwa gleich lang dauern. ![]()
Sprachabhängige Template-Imports
Zuerst die Erklärung, der vollständige Code folgt am Ende.
Um Internationalisierung (i18n) bzw. Übersetzung in Django-Templates zu bringen, gibt es verschiedene Möglichkeiten, allen voran die Möglichkeit, Strings mittels trans oder blocktrans zu markieren und in den entsprechenden gettext-Dateien zu übersetzen.
Das funktioniert gut und hat sich bewährt, solange es sich um Strings handelt. Möchte man eine komplette Seite übersetzen, inkl. Bildern und HTML-Code, wird es schwieriger, mal abgesehen davon, dass ich HTML in gettext-Strings einfach häßlich finde.
Eine Möglichkeit ist es, ifequal mit dem aktuellen Sprachcode des Users (LANGUAGE_CODE) zu vergleichen:
1 2 3 4 5 | {% ifequal LANGUAGE_CODE "de" %} <p>Hier ganz viel deutscher Text.</p> {% else %} <p>Here is a lot of english text, the default language.</p> {% endif %} |
Wird der Inhalt zu lang, kann man die Texte jeweils in eigene Templates auslagern und mittels include importieren:
1 2 3 4 5 | {% ifequal LANGUAGE_CODE "de" %} {% include "vieltext_deutsch.html" %} {% else %} {% include "vieltext_english.html" %} {% endif %} |
Hier setzt der unten gezeigte Templatetag an. Er arbeitet prinzipiell wie der Standard-Include-Tag aber importiert automatisch das Template mit der jeweiligen Sprache.
Der Templatename muss dabei das Format <templatename>.<languagecode> als Dateinamen besitzen. Angenommen unser User ist mit deutscher Spracheinstellung unterwegs und sein LANGUAGE_CODE wäre de, der Aufruf:
1 | {% langinclude "meintext.html" %} |
würde hier zuerst das Template meintext.html.de suchen und importierten. Schlägt dies fehl, importiert es wie gewohnt das Template meintext.html.
Hier nun der Templatetag:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 | from django.template import Library, Node from django.template import TemplateSyntaxError, TemplateDoesNotExist, Variable from django.template.loader_tags import IncludeNode from django.template.loader import get_template from django.conf import settings register = Library() class ConstantLanguageIncludeNode(Node): def __init__(self, template_path): self.template_path = template_path def render(self, context): try: t = get_template('%s.%s' % (self.template_path, context['LANGUAGE_CODE'])) except TemplateDoesNotExist, KeyError: t = get_template(self.template_path) except: if settings.TEMPLATE_DEBUG: raise return '' return t.render(context) def do_language_include(parser, token): """ Looks up for a template based on the template-name plus the current users language code. Loads the template and renders it with the current context. Example:: {% langinclude "foo/some_include.html" %} Based on the users LANGUAGE_CODE, assumed we have 'de', it tries to render the template 'foo/some_include.html.de'. If that doesn't exists, it renders the template 'foo/some_include.html'. This is the default behavior of the include-Tag. Basically this is a shortcut for the following code, just with a fallback for the default template:: {% ifequal LANGUAGE_CODE "de" %} {% include "foo/some_include.html.de" %} {% else %} {% include "foo/some_include.html" %} {% endifequal %} """ bits = token.contents.split() if len(bits) != 2: raise TemplateSyntaxError, "%r tag takes one argument: the name of the template to be included" % bits[0] path = bits[1] if path[0] in ('"', "'") and path[-1] == path[0]: return ConstantLanguageIncludeNode(path[1:-1]) return IncludeNode(bits[1]) register.tag('langinclude', do_language_include) |
Der Quellcode auf djangosnippets.org.
Tägliche Postfix-Helfer
Hier ist ein kurzer Überblick über Tools, die einem Postfix-Postmaster das tägliche Leben erleichtern. Alle Tools sind auf Debian und Ubuntu-Systemen in den Standard-Paketquellen enthalten.
pflogsumm - Tägliche Zusammenfassung der Logfileaktivität
pflogsumm erstellt eine detaillierte Übersicht über die Logfileaktivität des Tages oder des Vortages. Dazu gehört die Zusammenfassung aller Mails (ausgehend, eingehend, rejected etc.), deren Gründe, die Warnungen, Fehler und Panikattacken von Postfix und noch vieles mehr. Der tägliche pflogsumm-Bericht ist Pflicht für jeden, der einen Überblick über seine Mailserveraktivität haben möchte!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | Postfix Log Zusammenfassung fuer Feb 18
Zusammenfassung
---------------
Nachrichten
3168 empfangen
1709 zugestellt
24 weitergeleitet
23 zurueckgewiesen (83 Zurueckweisungen)
113 zurueckgeworfen
36233 abgelehnt (95%)
0 Ablehnungswarnungen
0 wartend
0 verworfen (0%)
440838k bytes empfangen
475401k bytes zugestellt
770 Sender
553 sendende hosts/domains
389 Empfaenger
183 empfangende hosts/domains
... [5000 weitere Statistikzeilen]
|
Am besten installiert man sich einen Crontab, der einem jeden morgen die Statistiken des Vortages zumailt. Hier wird morgens um 06:00 der Bericht des Vortages erstellt:
1 | 0 6 * * * /usr/sbin/pflogsumm -d yesterday /var/log/mail.info /var/log/mail.info.0 | mail -s "Mailstatistiken" postmaster@localhost |
- Homepage: jimsun.linxnet.com/postfix_contrib.html
- Deutsche Version von Patrick Koetter: postfix.state-of-mind.de/patrick.koetter/pflogsumm/
- Installation:
apt-get install pflogsumm
Mailgraph - Mailstatistiken in Bunt
Mailgraph ist ein Dämon der permanent das mail.log überwacht und Statistiken über empfangene und versandte Mails in einer RoundRobin-Datenbank speichert. Mit dabei ist das Perlscript mailgraph.cgi das aus diesen Daten wunderschöne Grafiken erstellt:
Das Script befindet sich von Haus aus in /var/lib/cgi-bin/mailgraph.cgi. Eventuell musst du es erst bearbeiten und die Pfade zur RRD-Datenbank (/var/lib/mailgraph/) anpassen.
Prüft nach der Installation auch noch einmal den Pfad zum Logfile; in der /etc/default/mailgraph lautet der Pfad für ein Standard-Setup MAIL_LOG=/var/log/mail.info.
Ob und inwiefern das sinnvoll ist, mag jeder für sich selbst entscheiden. Ich nutze es als schnellen Blick um mich zu vergewissern, dass mit dem Server alles rund läuft.
- Homepage: mailgraph.schweikert.ch
- Installation:
apt-get install mailgraph; /etc/init.d/mailgraph start
qshape - Queue-Statistiken
Mit Postfix wird das kleine Tool qshape mitgeliefert, bei Debian-Installationen ist es unter /usr/sbin/qshape zu finden. qshape zeigt eine Summierung der Anzahl der E-Mails in der Queue. Am interessantesten ist wohl die deferred-Queue:
1 2 3 4 5 6 7 | # qshape deferred
T 5 10 20 40 80 160 320 640 1280 1280+
TOTAL 59 0 0 0 0 1 0 56 2 0 0
example.com 56 0 0 0 0 0 0 56 0 0 0
h.cx 1 0 0 0 0 1 0 0 0 0 0
surfeu.de 1 0 0 0 0 0 0 0 1 0 0
couttsbath.com 1 0 0 0 0 0 0 0 1 0 0
|
So erhält man schnell einen Überblick, was auf dem Server hängt.
pfqueue - Frontend für die Queues
Eine alternative Oberfläche für qshape ist pfqueue. Dort wechselt man mit den Tasten 1, 2, 3 und 4 zwischen den Queues (deferred, active, incoming, hold). Mails kann man direkt mit d löschen, mit r requeuen und vieles mehr.
pfqueue ist vom Handling her ungewöhnlich aber sehr mächtig. Am besten man druckt sich vor der Benutzung die Manpage aus.
- Homepage: pfqueue.sourceforge.net
- Installation:
apt-get install pfqueue
Variable Spamchecks mit Postfix trotz content_filter
In den letzten Tagen hatte ich Gelegenheit meine Postfix-Konfiguration neu aufzubauen. Bisher hatte ich amavisd-new als Frontend-Filter für Spamassassin benutzt, in Ubuntu 8.04 funktioniert die Installation allerdings nicht mehr:
1 2 3 4 5 6 | Starting amavisd: ERROR: MISSING REQUIRED BASIC MODULES: Compress::Zlib BEGIN failed--compilation aborted at /usr/sbin/amavisd-new line 171. (failed). invoke-rc.d: initscript amavis, action "start" failed. WARNING: Starting amavisd-new failed. Please check your configuration. |
Das CPAN-Modul respektive libcompress-zlip-perl ist installiert, vielleicht weis ein Leser ja Rat.
Auf jeden Fall muss eine Alternative her. Im Spamassassin-Wiki ist eine Methode beschrieben, wie man Spamassassin als globalen content_filter in Postfix einbindet. Prinzipiell ganz gut, allerdings werden so alle Mails gefiltert, die Postfix passieren. Ich möchte aber einige User und auch ganze Domains ausschließen.
Installation
Zu allererst muss sichergestellt sein, dass Spamassassin auch installiert ist, auf Debian-Systemen reicht ein apt-get install spamassassin. Starte den Dämon mittels /etc/init.d/spamassassin start.
In der /etc/postfix/master.conf wird ein neuer Transport spamcheck hinzugefügt:
1 2 | spamcheck unix - n n - - pipe
user=nobody argv=/usr/bin/spamc -f -e /usr/sbin/sendmail -oi -f ${sender} ${recipient}
|
In die /etc/postfix/main.conf werden zwei Änderungen vorgenommen. Das Recipient-Limit für den genannten Spamassassin-Transport auf 1 setzen, damit jeweils nur eine E-Mail durchgereicht wird. Und als vorletzten Parameter – vor dem abschließenden permit – wird ein access_check auf die Datei /etc/postfix/access_spamassassin hinzugefügt.
1 2 3 4 5 6 7 8 9 10 11 | spamcheck_destination_recipient_limit = 1
smtpd_recipient_restrictions =
reject_non_fqdn_recipient,
reject_non_fqdn_sender,
permit_mynetworks,
# ... RBL Listen, etc
reject_unauth_destination,
# Hier werden die Empfänger geprüft
check_recipient_access hash:/etc/postfix/access_spamassassin,
permit
|
Zu guter letzt lege die Datei /etc/postfix/access_spamassassin an. Ihr ihr wird penibel definiert, welche Empfänger letztendlich durch Spamassassin geprüft werden:
1 2 3 | example.org FILTER spamcheck:dummy user@example.com OK example.com FILTER spamcheck:dummy |
Der Aufbau ist einfach, an erster Stelle steht die Empfängeradresse oder Domain gefolgt von einem Leerzeichen oder Tab und dem Wort FILTER spamcheck:dummy. spamcheck ist unser oben definierter Transport, dummy gibt den nächsten Hop an, den die Mail nehmen soll. In unserem Fall ist es ja der localhost, in diesem Fall reicht einfach dummy. Wenn einzelne Benutzer ausgeschlossen werden sollen, setze ein OK dahinter, achte aber auf die Reihenfolge.
Nicht vergessen, danach ein postmap hash:/etc/postfix/access_spamassassin auszuführen.
Was passiert bei diesem Setup?
Ein fremder Mailserver liefert die E-Mail ein, sofern sie alle Checks besteht passiert sie den Spamassassin-Recipient-Check und wird weitergeleitet zu spamc (rote Linie), einem Helferprogramm dass den Mailbody mit Spamassassin prüft. spamc schickt danach die Nachricht über das lokale sendmail Programm wieder an Postfix (blaue Linie).
Da die Nachricht diesmal vom lokalen System kommt, durchläuft sie nicht alle Checks sondern wird schon bei der Prüfung auf lokale IPs permit_mynetworks an den Haupttransport (virtual_transport, local_transport, etc.), wie zum Beispiel Procmail, abgegeben.
Steht die Zieladresse erst garnicht in der access-Datei, wird sie auch nicht mit Spamassassin geprüft und mittels des abschließenden permit an den originalen Haupttransport weitergeleitet (graue Linie).
Nachteile
Der große Nachteil bei diesem Setup ist, dass die Mail, bevor die spamc zugestellt wird, die Postfix-Queue verlässt. Es ist danach nicht mehr möglich, die Mail zu rejecten – jedenfalls nicht ohne Backscatter zu erzeugen.
Gerade das ist der große Vorteil von Amavis, wenn er als smtpd_proxy_filter eingebunden ist. So lassen sich dort alle oder bestimmte Mails ab einer gewissen Score rejecten. Da ich aber recht aggressiv Blacklisten einsetze ist die False-Positive Rate bei Spamassassin-erkannten Mails recht hoch, so dass ich damit leben kann, alle Mails letztendlich anzunehmen.
In der nächsten Zukunft werde ich mir mal Gedanken machen (sprich Google fragen), ob man das oben gezeigte Schema nicht auch als smptd_proxy_filter verbauen kann.
Meine Postfix-Konfiguration
Konfigurationsdateien sind ja mindestens so spannend wie Screenshots. Da ich keine Sicherheitsbedenken habe sie zu veröffentlichen und sie dazu noch kommiertiert ist; hier ist meine komplette Postfix-main.conf. Vielleicht findet noch jemand Ideen darin.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 | # -------------------------------------------------------------------- # Base Server Config # -------------------------------------------------------------------- myhostname = schwammkopf.mahner.org mydestination = localhost # myhostname steht in den virtual domains myorigin = /etc/mailname mynetworks = 127.0.0.0/8, 88.198.109.76, [::ffff:127.0.0.0]/104, [::1]/128 inet_interfaces = all smtpd_banner = $myhostname ESMTP - Der schwammige Mailserver append_dot_mydomain = no # Biff Tannen? Niemals! biff = no # foo+bar@example.com, brauch ich nicht recipient_delimiter = # Deutsche Bounce-Files bounce_template_file = /etc/postfix/bounce-templates/bounce.de-DE.cf # Systembenutzer alias_maps = hash:/etc/aliases alias_database = hash:/etc/aliases # -------------------------------------------------------------------- # SMTP Errorlimits # -------------------------------------------------------------------- # Maximale E-Mail Größe (TODO: auf 2kb setzen) 50MB message_size_limit = 52428800 # Wann soll eine Warnung erfolgen, wenn die Mail # nicht zugestellt werden kann? delay_warning_time = 1h # Wie lange soll die Mail in der Queue bleiben, # wenn keine Zustellung möglich ist? maximal_queue_lifetime = 1d bounce_queue_lifetime = 1d # Max und Min Zeit wenn der Connect fehlschläft (auch 4xx) # Greylisting ist typischerweise auf 5min eingestellt minimal_backoff_time = 330s maximal_backoff_time = 8000s # Wie lange nach dem HELO warten? smtp_helo_timeout = 60s # Wie viele Adressen in einer Nachricht maximal? # Mailman ftw! smtpd_recipient_limit = 16 # Wie viele Fehler bis wir müde werden und wie viele, # bis komplett blockiert wird smtpd_soft_error_limit = 3 smtpd_hard_error_limit = 12 smtpd_error_sleep_time = 1s # Ein nettes Hallo beim Connect bitte smtpd_helo_required = yes # Spammerzeit vergeuden smtpd_delay_reject = no disable_vrfy_command = yes # -------------------------------------------------------------------- # SMTP Checks & Restrictions # -------------------------------------------------------------------- smtpd_client_restrictions = smtpd_helo_restrictions = smtpd_sender_restrictions = smtpd_data_restrictions = smtpd_recipient_restrictions = check_client_access hash:/etc/postfix/access/client, check_helo_access hash:/etc/postfix/access/helo, check_sender_access hash:/etc/postfix/access/sender, check_recipient_access hash:/etc/postfix/access/recipient, # Blocken wenn Pipelining nicht angefragt wurde reject_unauth_pipelining, # Auf anständige Syntax prüfen reject_non_fqdn_recipient, reject_non_fqdn_sender, # DNS Checks reject_unknown_recipient_domain, reject_unknown_sender_domain, # Eigene Nutzer erlauben permit_mynetworks, permit_sasl_authenticated, # RBL Blacklisten reject_rbl_client zen.spamhaus.org, reject_rbl_client ix.dnsbl.manitu.net, reject_rbl_client bl.spamcop.net, reject_rbl_client blackhole.securitysage.com, # Greylisting check_policy_service inet:127.0.0.1:60000, # Relaying reject_unlisted_recipient, reject_unauth_destination, # Spamassassin Content Filter check_recipient_access hash:/etc/postfix/access/spamassassin, # Wer nicht in spamassassin steht, ist durch permit # -------------------------------------------------------------------- # TLS Empfang/Versand # -------------------------------------------------------------------- # smptd (Mailempfang) smtpd_tls_security_level=may smtpd_tls_cert_file=/etc/postfix/certs/cert.pem smtpd_tls_key_file=/etc/postfix/certs/key.pem smtpd_tls_CAfile = /etc/postfix/certs/CAcert.pem smtpd_tls_loglevel = 1 # Bei SSL-Verbindung diese im received-Header markieren smtpd_tls_received_header = yes # smtp (Mailversand) smtp_tls_security_level=may smtp_tls_cert_file=/etc/postfix/certs/cert.pem smtp_tls_key_file=/etc/postfix/certs/key.pem smtp_tls_CAfile = /etc/postfix/certs/CAcert.pem smpt_tls_loglevel = 1 # -------------------------------------------------------------------- # SASL (SMTP-Auth) # -------------------------------------------------------------------- smtpd_sasl_type=dovecot smtpd_sasl_path=private/auth smtpd_sasl_auth_enable=yes smtpd_sasl_security_options = noanonymous # -------------------------------------------------------------------- # Transport and Mapping # -------------------------------------------------------------------- # Dovecot transport virtual_transport = dovecot dovecot_destination_recipient_limit = 1 # Spamassassin Transport in den recipient restrictions spamassassin_destination_recipient_limit = 1 virtual_uid_maps=static:5000 virtual_gid_maps=static:5000 virtual_mailbox_domains = mysql:/etc/postfix/mysql/virtual-mailbox-domains.cf virtual_mailbox_maps = mysql:/etc/postfix/mysql/virtual-mailbox-maps.cf virtual_alias_maps = mysql:/etc/postfix/mysql/virtual-alias-maps.cf, mysql:/etc/postfix/mysql/email2email.cf |
Plesk 8.x und 9.x: Keine Rechte zum Datenbank-Anlegen trotz Admin-Account
In Plesk 8.x und auch noch in Plesk 9.0.x erscheint eine Fehlermeldung, wenn man eine neue Datenbank anlegen will:
Fehler: Parallels Plesk Panel kann keine Verbindung zum Datenbankserver herstellen, da kein gültiger Administrator-Benutzername und -Passwort angegeben wurden.
Großartige Fehlermeldung. Natürlich sind die Rechte vorhanden und die Limits nicht ausgereizt. Der Fehler tritt nur auf, wenn man einen Webkit-Browser bzw. Safari benutzt. Also am einfachsten für diese Aufgabe zu Firefox wechseln.
Mit Google-Chrome tritt der Fehler nicht auf, wohl weil es eine andere Javascript-Engine als Safari nutzt. Was wiederum bedeutet, dass Plesk wohl die DB-Zugangsdaten irgendwo im Javascript mitschleift und dort dann nicht findet. Alles sehr komisch.
Ordnung im Medienordner: Dynamische Upload-Pfade
Djangos FileField und das davon abgeleitete ImageField Feld enthalten ein upload_to-Argument das angibt, wohin die Datei im statischen Medienordner gespeichert werden soll:
1 2 3 4 5 6 7 8 | class Image(models.Model): image = models.ImageField(upload_to='images/') def __unicode__(self): return self.image.name def get_absolute_url(self): return self.image.url |
Ein hochgeladenes Bild wäre dann über die URL /media/images/filename.jpg zu erreichen. /media/ ist der URL-Pfad zu den statischen Medien, der zuvor in der settings.py mit MEDIA_URL definiert wurde.
upload_to muss aber kein festgelegter Pfad sein, es kann auch ein callable, also eine Funktion oder jede Klasse die ein __call__ Objekt bietet, verwerten.
Wozu braucht man das? Immer dann, wenn der Pfad dynamisch sein soll. Im folgenden Beispiel soll eine hochgeladene Datei in mehreren Unterordnern abgelegt werden, wobei die Buchstaben der Unterordner die Anfangsbuchstaben der Datei sind, Huh, kompliziert? Gar nicht:
1 2 3 4 5 6 7 8 9 10 11 | def bilder_in_unterordnern(instance, filename): if len(filename.rsplit('.')[0]) >= 3: # Dateiname ohne Extension muss # mindestens 3 Zeichen besitzen filename = "images/%s/%s/%s/%s" % (filename[0], filename[1], filename[2], filename) return filename class Image(models.Model): image = models.ImageField(upload_to=bilder_in_unterordnern) # ... |
Das upload_to Argument enthält den Namen einer Funktion, die den neuen Dateinamen (inklusive Pfad) zurück gibt. Beachte aber, dass das Funktionsargument hier keine Klammern besitzt, sonst würde sie nur einmal, beim Starten des Webservers, ausgeführt werden.
Diese callable-Funktion besitzt immer zwei Argumente, die aktuelle Instanz des Modelobjekts und den originalen Dateinamen. Der Rückgabewert ist der neue Dateiname.
Bilder die mit dem obigen Schema hochgeladen werden, würden dabei diese Pfade erhalten:
- bild.jpg > /media/images/b/i/l/bild.jpg
- foobar.jpg > /media/images/f/o/o/foobar.jpg
- ...
Schon ganz hübsch.
Richtig sinnvoll wird es aber erst, wenn der Bildpfad Werte aus der selben oder gar einer anderen Model-Instanz enthalten soll.
Angenommen die Bilder sind mit einem Artikel (von einem Weblog, natürlich) verknüpft und sollen als Dateipfad den Slug des verknüpften Artikels enthalten, so kann das Callable auf die aktuelle Model-Instanz und dessen Verknüpfungen zugreifen:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | def bild_mit_articleslug(instance, filename): return 'images/%s/%s' % (instance.article.slug, filename) class Article(models.Model): title = models.CharField(max_length=100, blank=False) slug = models.SlugField() content = models.TextField() def __unicode__(self): return self.title class Image(models.Model): article = models.ForeignKey(Article) image = models.ImageField(upload_to=bild_mit_articleslug) |
Angenommen unser Artikel hat den Titel "Mein neuer Blogbeitrag" so wären die Dateinamen so:
- bild.jpg > /media/images/mein-neuer-blogbeitrag/bild.jpg
- foobar.jpg > /media/images/mein-neuer-blogbeitrag/foobar.jpg
- ...
So schafft man Ordnung im Dateisystem. ![]()
Ausnahme Primärschlüssel
Eine Ausnahme ist, wenn der Dateiname des Bildes den Primärschlüssel des Objekts enthalten soll. Da, bei einem Insert, der Primärschlüssel noch nicht gesetzt ist, existiert dieses Attribut noch nicht. Ein Umweg ist, erst das Bild hochzuladen, das Objekt zu speichern dann das Bild entsprechend umzubenennen und das Objekt wiederum zu speichern:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | class Image(models.Model): image = models.ImageField(upload_to='tmp/') def save(self, *args, **kwargs): # Modelobjekt speichern und einen Primärschlüssel (self.pk) erhalten super(Image, self).save(*args, **kwargs) # Nur umbennenen wenn sich die Datei noch im tmp-Pfad befindet MEDIA_ROOT = os.path.abspath(settings.MEDIA_ROOT) if self.image.path.startswith(os.path.join(MEDIA_ROOT, 'tmp/')): # Neuen Dateinamen mit Pfad und Primärschlüssel generieren new_filename = '%s%s%s' % (os.path.join(MEDIA_ROOT, 'images/'), self.pk, os.path.splitext(self.image.name)[1]) # Letztendlich die Datei umbennenen und den neuen Pfad speichern old_filename = os.path.join(MEDIA_ROOT, self.image.name) os.rename(old_filename, new_filename) # Den neuen Dateinamen im Model setzen und noch einmal speichern self.image = new_filename super(Image, self).save(*args, **kwargs) |
Das ganze geht bestimmt noch hübscher und enthält einige Fallstricke, so muss der Ordner images/ schon existieren. Aber seis drum, Bilder die so hochgeladen werden, würden als Dateinamen den Primärschlüssel des jeweiligen Modelobjekts erhalten:
- bild.jpg > /media/images/1.jpg
- foobar.jpg > /media/images/2.jpg
- ...
Dateinamen mit Datum und Zeit
Der Vollständigkeit halber sei noch erwähnt, dass das upload_to Argument auch ohne Umwege strftime-Argumente verwerten kann. Ein upload_to='/images/%Y/%m/' würde das Bild im Ordner /images/2009/02/bild.jpg speichern.

