Rubys FastGettext ist tatsächlich schnell
Thursday, 19. December 2019
Ich war mal wieder am Performance-Debuggen von pc-kombo. Die Seite soll schneller laden, wenn der Cache noch nicht befüllt ist, was doch immer wieder Besucher trifft. Dabei stolperte ich über diesen Abschnitt des Flamegraphs:
tt
ist der Übersetzungshelfer, lru-redux der genutzte Cache, Grundlage das Gem i18n. Und dieser Abschnitt machte einen gewichtigen Teil des Seitenladevorgangs aus.
Also habe ich das alte Übersetzungssystem auf FastGettext umgestellt. Der Code in Sinatra sieht in etwa so aus:
helpers do include FastGettext::Translation def t(token, opts = {}) _(token.to_s) % opts end end configure do FastGettext.add_text_domain('pckombo', path: 'locales', type: :yaml) end before do FastGettext.text_domain = 'pckombo' if request.env['HTTP_ACCEPT_LANGUAGE'] languages = HTTP::Accept::Languages.parse(request.env['HTTP_ACCEPT_LANGUAGE']) languages.each do |language| case language.locale when /en[_]*/ FastGettext.locale = "us" break when /de[_]*/ FastGettext.locale = "de" break when /fr[_]*/ FastGettext.locale = "fr" break when /es[_]*/ FastGettext.locale = "es" break end end end end
Die alten yaml-Übersetzungen konnten weiterverwendet werden. So beginnt z.B. die locales/de.yml:
de: cpu: Prozessor
Es ist also fast eine einfach so einsetzbare Alternative mit minimalen Codeänderungen.
Das Ergebnis:
Die Übersetzungen beim ersten Laden brauchen nun einen Bruchteil der Zeit. Das beste daran: Das wird nicht nur den speziellen Seitenaufruf beschleunigen den ich da betrachtet hatte, sondern generell der gesamten Webseite helfen.
Wer Übersetzungen in Ruby umsetzen muss, für den ist FastGettext ist definitiv einen Blick wert.
Categories setzen mit Rubys RSS-Modul
Wednesday, 18. December 2019
Ruby kann ja RSS lesen und schreiben, aber die Dokumentation für das Modul ist ungenügend und es scheint generell wenig genutzt zu werden, sodass man auch wenig Hilfe findet. Ich stand jetzt vor einem Rätsel: Wie kann ich den Items in einem erstellten RSS-Feed Kategories hinzufügen, also category
setzen? Doch jetzt habe ich es gelöst. Und zwar geht das so:
rss = RSS::Maker.make("rss2.0") do |maker| … maker.items.new_item do |newItem| … categories = [{name: 'abc', scheme: 'https://www.example.com'}] categories.each do |category| target = newItem.categories.new_category target.content = category[:name] target.domain = category[:scheme] end end
Xapian/Omega in Ruby/Sinatra integrieren
Tuesday, 10. December 2019
Wie baut man am besten eine Suchmaschine für eine einzelne Webseite? In meinem Fall war das Szenario so: Ich habe eine Sammlung statischer HTML-Dokumente, will aber eine Suchfunktion anbieten. Also brauchte ich etwas, was die ganzen HTML-Dokumente indexiert und dann durchsuchen kann. Genau das kann Xapian. Speziell für Webseiten gibt es vom Xapian-Projekt die vorgefertigte Omega-Suchmachine.
Also, Xapian kann alles mögliche durchsuchen, man könnte dem Xapian-Index auch programmatisch Elemente der Datenbank hinzufügen. Und es muss nicht für eine Webseite sein, es kann auch sonstwo eingebunden werden – mancher mag sich an xapian als nervenden Prozess in Ubuntu erinnern. Ich aber wollte nur HTML-Seiten durchsuchbar machen und das online anbieten, und genau dafür ist Omega gedacht. Allerdings: Omega hat ein eigenes Webfrontend, das per CGI eingebunden wird. Das wollte ich nicht nutzen, denn ich habe sowieso schon ein zusätzliches dynamisches Backend laufen, und die Template-Funktion von Omega sah unschön und kompliziert aus.
Xapian und Xapian-Fu installieren
Xapian samt Omega sollte in den Quellen sein. Bei mir mit void:
sudo xbps-install xapian-omega
Dazu empfehle ich für die Sinatra-Integration das Gem xapian-fu. Also in die Gemfile:
gem 'xapian-fu'
und dann installieren mit bundle install
.
Allerdings reichte das nicht. Xapian-fu braucht die Ruby-bindings von xapian, und die waren bei mir nicht in den Quellen enthalten. Also muss in dem Fall noch das Archiv xapian-bindings heruntergeladen und die Ruby-Bindings kompiliert werden:
unp xapian-bindings-* cd xapian-bindings-VERSION ./configure --with-ruby cd ruby make sudo make install
Mit omindex indexieren
Omindex installierte sich in den PATH und ist einfach bedienbar. Meine HTML-Dateien liegen im Ordner public/, Bilder sollten ignoriert werden, dafür kam ich auf diesen Befehl:
omindex --db omega/data/default/ --filter image/*:skip public/
Der Code in Sinatra
Statt jetzt den CGI-Part von Omega zu nutzen übernimmt Sinatra mit Xapian-Fu die Suche:
require 'sinatra' require 'xapian-fu' include XapianFu get '/search' do searchterm = params['searchterm'] db = XapianDb.new(:dir => 'omega/data/default/', :create => false) results = db.search(searchterm).map{|match| {url: match.data.split("\n").detect{|x| x.start_with?('url=')}.to_s.sub('url=', ''), caption: match.data.split("\n").detect{|x| x.start_with?('caption=')}.to_s.sub('caption=', '')} } erb :search, locals: {searchresults: results, searchterm: searchterm} end
Man sieht: Der Code nimmt die Datenbank als Quelle, die vorher omindex angelegt hat.
Die Zuweisung in den results-Hash ist hässlich. Ich fand leider keinen Weg, mit xapian-fu die Suchergebnisse richtig strukturiert auszulesen. Nur match.data
ist zugänglich, womit man etwas anfangen kann. Der Weg über einen XapianDocValueAccessor, mit dem man wohl die Felder gezielt auslesen können sollte, funktionierte bei mir nicht. Eventuell codiert omindex den Suchindex zu speziell.
Die Suchergebnisse gehen dann an ein ERB-Template:
<h1>Search results for <%= h searchterm %></h1> <% if searchresults.size > 0 %> <ol id="searchresults"> <% searchresults.select{|item| ! item[:caption].empty? }.each do |result| %> <li> <a href="<%= result[:url] %>"> <span><%= result[:caption] %></span> </a> </li> <% end %> </ol>
Xapian/Omega ist keine Lösung, wenn die Webseite nur aus statischen HTML-Seiten bestehen darf. Aber es ist wohl eine gute Lösung wenn es einen echten Server gibt. Die Integration in Ruby/Sinatra ist relativ einfach.
Dass die xapian-bindings nicht einfach per gem installierbar sind ist der problematischste Punkt, auch das vorgelagerte indexieren wird nicht in jedes Projekt gut passen. Ohne statischen Webseitengenerator im Zentrum würde ich omega und omindex ignorieren und stattdessen nur mit xapian-fu arbeiten. Dann kämen die Daten eben direkt aus der Datenbank.
Hier aber war das Parsen der HTML-Seiten die perfekte Lösung.
Alternativer Startpunkt: Generator statischer Seiten
Tuesday, 5. November 2019
Ich versuche diesmal etwas anderes. Normalerweise baue ich Webanwendungen mit Ruby um Sinatra herum, zum Beispiel die Blogsoftware ursprung. Auch der PC-Hardwareempfehler pc-kombo ist im Grunde so eine Ruby/Sinatra-Anwendung. Aber man will ja nicht immer das gleiche machen, sondern dazulernen und neue Lösungen ausprobieren. Bei Pipes z.B. war das der grafische Editor. Gerade versuche ich mich daher an einem statischen Seitengenerator als technischen Kern eines neuen Projekts.
Worum geht es dabei? Performance und Architektur. Zum einen verspreche ich mir eine schnellere Seite, wenn ihr Kern statisches HTML ist und nur von nginx ausgeliefert wird, ohne Datenbankabfrage oder Kontakt mit Ruby. Zum anderen will ich auch sehen, wie anders die Daten strukturiert werden können und wie viel kleiner der Code wird, wenn der Fokus mehr auf das generierte HTML und CSS und eventuell Javascript liegt und beim Seitenaufruf nicht direkt auf die Datenbank zugegriffen werden muss.
Bisher setzte ich sehr auf Konfiguration. So gibt es einen Ordner pages/ mit JSON-Dateien, die bestimmen welche HTML-Seiten erstellt werden sollen. Als Beispiel die gpu.json:
{ "view": "gpus", "target": "gpus.html", "collection": "gpus" }
Die Felder hier bestimmen das Verhalten des Generators: view
welches Template verwendet wird, target
welche Datei erstellt werden soll, collection
welche Datenbankdaten dem Template mitgegeben werden sollen.
Dazu gehören noch zwei Templates: Zuerst die implizit aufgerufene layout.erb:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8" /> <title>generatortest</title> </head> <body> <%= yield %> </body> </html>
Und die gpus.erb:
<h2>Gpus</h2> All the gpus we know: <ol> <% collection.each do |item| %> <li><%= item[:name] %></li> <% end %> </ol>
All das beachtet die generator.rb:
require 'erb' require 'tilt' require 'json' require 'moneta' store = Moneta.new(:Memory) store[:gpus] = [{id: 1, ean: '123', name: 'WindForce RTX 2080'}, {id: 2, ean: '456', name: 'Sapphire RX 590'} ] # foreach page definition in pages, get the specified data collection, give it to the template and save the html at the target destination Dir.glob(File.expand_path('../pages/*.json', __FILE__)).each do |file| control = JSON.parse(File.new(file).read, symbolize_names: true) collection = store[control[:collection].to_sym] if control[:collection] view = File.join('../views/', control[:view] + '.erb') view = File.expand_path(view, __FILE__) if File.exists?(view) layoutpath = File.expand_path('../views/layout.erb', __FILE__) layout = Tilt::ERBTemplate.new(layoutpath) output = layout.render { # this construction ensures the layout is used template = Tilt::ERBTemplate.new(view) # the final render has nothing to evaluate but needs the # the local collection, thus the empty Object as param template.render(Object.new, collection: collection) } targetpath = File.join('../public/', control[:target]) targetpath = File.expand_path(targetpath, __FILE__) File.write(targetpath, output) end end
Man sieht: Das ist absolutes Anfangsstadium. Aber es funktioniert schonmal! Nach Aufruf des Generators landet fertiges HTML im Ordner public/. Was dann der wäre, den der Webserver ausliefern könnte, inklusive dieser gpus.html:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8" /> <title>generatortest</title> </head> <body> <h2>Gpus</h2> All the gpus we know: <ol> <li>WindForce RTX 2080</li> <li>Sapphire RX 590</li> </ol> </body> </html>
Überlegungen
Um zwei Entscheidungen habe ich mich gedrückt bzw sie ungewöhnlich flexibel gelassen. Mit Moneta vermeide ich die Entscheidung für eine Datenbank. Normalerweise wäre das sqlite, aber ich bin noch nicht sicher ob das hier wirklich passt oder ob es mehrere Prozesse geben wird. Deswegen landen die Datenbanktestdaten bisher auch nur im Arbeitsspeicher. Und tilt würde es später einfach machen, statt erb eine andere Templatesprache zu benutzen.
Bedenken habe ich noch wegen der Filter. Ich weiß, dass ich später Nutzer Listen – wie die im Beispiel erstellte – filtern lassen will. Soll das komplett per Javascript geschehen? Oder baue ich dafür Seiten ein, die mit Ajax auf die angepassten Datenbanksets zugreifen? Dass es einen dynamischen Teil der Anwendung geben wird ist immerhin bereits vorgesehen, als Backend für den Administrator.
Ob der Seitengenerator nicht ein Monster werden wird? Ich werde versuchen müssen ihn schlank zu halten. Andererseits sehe ich gerade nicht, dass ihm noch viel fehlen würde, um die Seite umzusetzen die ich bauen will.
Erstes Fazit
Das könnte funktionieren. Wie schlüssig sich das Konzept anfühlt und wie schnell dieser Prototyp zu bauen war erklärt mir etwas, warum die letzten Jahre diese static site generators so beliebt wurden. Es ist eben nicht nur etwas für Blogs oder eine Homepage. Die HTML-Erstellung nicht erst beim Seitenaufruf zu machen ist auch generell ein gutes Prinzip.
Aber es wird sich zeigen, ob mit diesem Weg dann auch wirklich eine performante und wartbare Webanwendung entsteht.
Rubys Twitter-Gem: Rate Limit exceeded vermeiden
Saturday, 31. August 2019
Wenn man mit dem Twitter-gem sucht, ist es einfach in das Rate-Limit zu rennen. Nicht nur, dass man sich darum selbst kümmern muss nicht zu viele Abfragen zu starten: Selbst dann noch löst ein Suchvorgang viele Requests auf einmal aus.
Twitters API nutzt cursoring, das heißt sie gibt dir erstmal nur 100 Ergebnisse und dann einen Verweis auf die nächste Seite, auf der wieder 100 Ergebnisse stehen können, und so weiter. Jeder Abruf einer Seite ist ein Request, und der Twitter-Gem folgt diesem Cursor automatisch:
# @return [Enumerator] def each(start = 0) return to_enum(:each, start) unless block_given? Array(@collection[start..-1]).each do |element| yield(element) end unless last? start = [@collection.size, start].max fetch_next_page # hier each(start, &Proc.new) end self end
Leider gibt es im Gem keinen Parameter um das zu vermeiden. Also musste ich den Code selbst anpassen: In lib/twitter/search_results.rb wird aus
# @return [Boolean] def last? !next_page? end
ein
# @return [Boolean] def last? true end
Dann wird zumindest bei Suchabfragen keinem Cursor gefolgt. Das begrenzt dann auch die Suchergebnismenge auf 100, aber das ist in meinem Anwendungsfall okay.
Pipes, Stripe und die SCA-Richtlinie
Friday, 9. August 2019
Strong Customer Authentication (SCA) – schon vor einer Weile hatte Stripe vielen Kunden und auch mir geschrieben, dass dadurch Änderungen anstehen. Zukünftig werde das vorgeschrieben sein (schon im September) und daher musste die Stripe-Integration von Pipes angepasst werden. Heute kam noch einmal solch eine sehr warnende Email, ich habe mir das angeschaut und tatsächlich den Bezahlvorgang angepasst.
Aber trotz eigentlich geringen und positiven Entwicklungsaufwands war der Gesamtprozess nicht toll. Stripe verweist auf diese Dokumentation und erklärt dort ganz viel, aber wenig konkretes. Mich interessieren doch nur zwei Dinge:
- Was muss ich tun, damit Nutzer von Pipes weiterhin ein Abonnement abschließen können?
- Muss ich etwas tun, damit bestehende Abonnements weiterhin monatlich/jährlich abgebucht werden?
Das ist für Stripe sicher schwierig im Voraus zu beantworten, weil die Antwort nicht bei jedem Kunden gleich ist. Aber die Dokumentation scheint mir auch nicht auf die Beantwortung dieser Fragen ausgelegt. Damit bin ich etwas unzufrieden.
Wie auch immer, der erste Schritt war in meinem Fall relativ einfach. Pipes nutzt Stripe Checkout. Das ist eine recht komfortable Softwarelösung, ein bisschen Javascript und etwas Servercode und schon hat man dieses vielleicht bekannte Kreditkarteneingabefenster und kann damit bezahlen bzw Zahlungen entgegennehmen. Also wird das automatisch aktualisiert und Checkout-Integratoren müssen nichts machen? Nein! Stattdessen ist das kleine Fenster jetzt die Legacy-Variante, das neue Checkout funktioniert etwas anders, löst aber die Authentifizierung.
Und genau das hätte mir Stripe in der Email oder der SCA-Dokumentation auch direkt sagen können. Stattdessen ist es in der Checkout-Doku versteckt.
Dabei ist die neue Lösung nichtmal schlecht. Sie sieht hübsch aus und es ist weniger eigener Code notwendig.
Es gibt eine vernünftige Wechselanleitung. Aber im Grunde wechselt man erstmal das Javascript aus. Vorher basierte das auf einem Formular:
<form action="/subscribe" method="POST"> <script src="https://checkout.stripe.com/checkout.js" class="stripe-button" data-key="pk_test_..." data-name="pipes.digital" data-description="Regular Plan" data-amount="20" data-currency="eur" > </script> </form>
Jetzt wird eine Funktion aufgerufen:
<script src="https://js.stripe.com/v3"> <button id="checkout-button">Subscribe <script> var stripe = Stripe('pk_test_...'); var checkoutButton = document.querySelector('#checkout-button'); checkoutButton.addEventListener('click', function () { stripe.redirectToCheckout({ items: [{ plan: 'regular_v1', quantity: 1 }], customerEmail: 'customer@example.com', successUrl: 'https://pipes.digital/sub_success', cancelUrl: 'https://pipes.digital/pricing' }); }); </script>
Klar, bei mir liegt das alles in erb-Templates und sieht daher etwas anders aus.
Vorher musste auf dem Server dann noch das Abonnement angelegt werden:
post '/charge' do customer = Stripe::Customer.create( :email => authorized_email, :source => params[:stripeToken] ) subscription = Stripe::Subscription.create( :customer => customer.id, :items => [ { :plan => params[:plan] }, ], ) end
Nun:
Das entfällt völlig! Da das neue Checkout auf eine Seite bei Stripe weiterleitet wird alles dort erledigt.
Trotzdem muss noch auf dem eigenen System darauf reagiert werden, dass der Nutzer etwas bestellt hat. Bei Pipes wird ein Datenbankeintrag angelegt. Dafür gibt es einen Webhook (einen Ort, an dem ein anderer Server einen POST hinsenden kann), der in der Doku wieder gut erklärt wird. Prinzipiell aktiviert man ihn im Stripe-Dashboard und fängt ihn dann so:
# webhook endpoint for stripes post '/webhook' do payload = request.body.read event = nil # Verify webhook signature and extract the event # See https://stripe.com/docs/webhooks/signatures for more information. sig_header = request.env['HTTP_STRIPE_SIGNATURE'] begin event = Stripe::Webhook.construct_event( payload, sig_header, endpoint_secret ) rescue JSON::ParserError => e # Invalid payload warn "invalid webhook payload" status 400 return rescue Stripe::SignatureVerificationError => e # Invalid signature warn "invalid webhook signature" status 400 return end # Handle the checkout.session.completed event if event['type'] == 'checkout.session.completed' session = event['data']['object'] # Fulfill the purchase: plan = session['display_items'][0]['plan']['id'] if plan == 'regular_v1' User.new(email: session['client_reference_id']).promoteToRegular(subscription_id: session['subscription']) end end status 200 end
Es gibt natürlich noch viel mehr Events, die man mit diesem Webhook fangen könnte, zum Beispiel wenn Zahlungen fehlschlagen.
Und die zweite Frage? Ob bestehende Abonnements weiterlaufen blieb mit bisher unklar. Es gibt da zwar einen Abschnitt für, aber ob dieses Grandfathering passieren kann hängt wohl davon ab, ob das Abo so erstellt wurde:
Ihr saht den Code oben, ist das bisher passiert? Der verlinkte Beispielcode sagt ja, aber nichts davon wird explizit im Code erledigt.
Meine jetzige Annahme ist, dass beim Erstellen von Abonnements gemäß des alten Checkout-Verfahrens die Zahlungen weiterlaufen sollten. Man müsste aber wohl eigentlich auf Nummer sichergehen und eine Seite bauen, die über die Stripe-API den Kunden die Möglichkeit gibt, die Authentifizerung nachzuholen. Eine so simple Lösung wie Checkout gibt es dafür aber nicht. Im Fall von Pipes würde sich das Abtauchen in die API kaum lohnen, im Fall der Fälle würde ich dann eher die Nutzer bitten, nochmal neu zu abonnieren.
Naja, spätestens im September wird sich das aufklären.
Gesammelte Update-Wehwechen nach der Serendipity-Woche
Tuesday, 19. February 2019
Alles in allem lief der Blog nach dem Upgrade auf die Alpha gut, aber es gab doch noch ein paar Probleme.
Nicht alle hier genutzten Plugins waren kompatibel mit PHP 7.2. Zuerst bemerkte ich das Sitemaps-Plugin, damit installiert konnte ich keine Artikel abspeichern. Ich patchte das, aber es ist ein notdürftiger Patch und das Plugin braucht einen Maintainer, der es mal durchtestet und aufräumt. Dafür steht jetzt auch ein Issue auf Github.
Das zweite problematische Plugin war mein Bayes-Plugin, das beim Lernen eines Kommentars als Spam oder valid einen Fehler wegen einer Konstante warf, die ein String sein müsste. Der Patch ist hier und damit schon in Spartacus.
Problematischer waren ein paar Installationsspezifische Besonderheiten. Mein Blog benutzt je ein eigenes Theme, einen modernisierten Fork von codeschmiede. Nach dem Upgrade war die Kommentardarstellung falsch, das Datum fehlte und die Seitenleiste sah ebenfalls kaputt aus. Aber nicht direkt, erst nachdem ich wegen eines anderen Fehlers templates_c/ leerte. Tatsächlich war da mein Theme etwas kaputt, was ich dann auch im Testblog nachvollziehen konnte, aber vorher im Blog wohl wegen gecacheter Smarty-Dateien nicht bemerkte.
Drei Commits adressierten weitere Fehler im Kern. Erstmal mussten die Funktionen des internen Caches neu strukturiert werden, weil ich vorher die Cachedauer nicht gesetzt hatte. Jetzt sitzen die Funktionen auch an einer besseren Stelle im Kern und sind so leichter anderswo nutzbar. Dann fand ich einen weiteren Konstanten-Fehler. Und schließlich bemerkte Matthias, dass Seiten die eine 404-Fehlerseite anzeigen sollten stattdessen einen internen Fehler produzierten, was angesichts des Ausbleibens des Fehlers mit 2k11 scheinbar auch mit meinem Theme zusammenhängt. Der simple Patch war leider schwer gefunden.
Es bleibt noch ein Fehler: Gerade kann ich keine Preview dieses Artikel erstellen. Die Fehlermeldung besagt
Ihr Browser hat keinen gültigen HTTP-Referrer übermittelt. Dies kann entweder daher kommen, dass Ihr Browser/Proxy nicht korrekt konfiguriert ist, oder dass Sie Opfer einer "Cross Site Request Forgery (XSRF)" waren, mit der man Sie zu ungewollten Änderungen zwingen wollte. Die angeforderte Aktion konnte daher nicht durchgeführt werden.
Sowas sah ich auch vorher schonmal, aber dann half immer ein erneuten Klicken auf den Vorschau-Button, diesmal nicht. Das könnte an PHP 7.2 hängen oder auch an meiner Überarbeitung des Autologin-Tokens, wobei seltsam ist dass es in meinem Testblog nicht einmal auftrat. Da muss ich nochmal ran.
Der letzte Tag der Entwicklungswoche: Hier läuft jetzt Serendipity-2.2.1-alpha2 und PHP 7.2
Friday, 15. February 2019
Die Installation war der letzte Schritt. Ich hatte ja gestern schon angekündigt, damit die Woche abschließen zu wollen. Was nicht völlige Inaktivität bedeuten soll, aber andere Projekte (Pipes!) kriegen erstmal wieder Priorität.
Das Upgrade auf die aktuelle Version funktionierte, was aber auch kein Wunder ist wenn der Upgrader fast keine Aufgabe durchführen muss. Allerdings begrüßte mich der Blog danach mit einer Fehlermeldung: Er konnte voku/simple-cache nicht laden. Ich hatte vergessen die aktualisierten composer-Dateien hochzuladen, dass das nötig ist war mir gar nicht klar. Schnell nachgeholt scheint erstmal alles zu laufen, wenn nicht wird sich das jetzt zeigen. Aber ich bin zuversichtlich, dass der Code jetzt mindestens reif für eine Beta ist.
Mir hat die Woche Spaß gemacht. Serendipity ist eben ein altes PHP-Projekt, mit allen Vor- und Nachteilen. Zu den Nachteilen gehören die vielen Workarounds für teils jahrealte PHP-Absonderlichkeiten und die Codestruktur und Qualität mancher Ecken im Kern, die dann eben den Standards von PHP-Projekten vor 15 Jahren entsprechen.
Aber die Vorteile zählen eben auch: Es ist ein stabiles und simpel gehaltenes Framework, das extrem aufs Bloggen ausgelegt ist, in dem sich manche Features erstaunlich komfortabel umsetzen lassen. Mit jeder neuen PHP-Version seit PHP 7 bekommen wir eine ganze Menge Verbesserungen der Sprache und in ihrem Ökosystem umsonst, die einzubauen und generell den Code zu verbessern ist lohnenswerte Arbeit. Auch haben sich meiner Meinung nach in den letzten Jahren viele Verbesserungen im Kern angesammelt, welche die Weiterentwicklung desselben, aber auch die Pluginentwicklung besser machen. Zusammengenommen ist die Arbeit an Serendipity derzeit besonders erfreulich. Ich kann jeden nur einladen, sich mal an einer neuen Funktionalität, einem Bugfix oder einem Plugin zu probieren.
Serendipity-Entwicklungswoche, Tag 6: Kleine Verbesserung der Mediendatenbank und des Podcast-Plugins
Thursday, 14. February 2019
Heute reichte es nur für eine kleine gestern überlegte Verbesserung der Mediendatenbank und ich muss mich beim Aufschreiben beeilen, um nicht in den letzten Tag der Entwicklungswoche zu rutschen.
IDs für Nicht-Bilder
Mindestens der aktuelle Code der Mediendatenbank könnte auch Links in Einträgen zu per Mediendatenbank eingefügten Dateien umbiegen, wenn diese umbenannt oder verschoben werden. Dateien meint hier Dateien im Gegensatz zu Bildern, denn bei Bildern geht das ja schon lange. Doch bisher wurden die Dateien ignoriert weil sie beim Einfügen keine ID mit in den Eintrag geschrieben bekommen haben. Das habe ich nun geändert und getestet, dass das Umbenennen tatsächlich den Link umschreibt.
IDs für das Podcast-Plugin
Die IDs für Audiodateien im Eintrag zu haben ist eine Voraussetzung für das Podcast-Plugin, das jetzt die ID auslesen und damit auf die Datei auf dem Server zugreifen kann. Ich habe den Code dafür nur angelegt und damit schonmal den Wrapper für den Player angepasst, das war vorher auf Übersichtsseiten wie der Hauptseite des Blogs noch kaputt. Der nächste Schritt wäre mittels der ID die Originaldatei auszulesen und so Informationen wie die Länge der Aufzeichnung ins Markup des Players aufnehmen zu können.
Unschön an dem ganzen ist, dass ich nach der Entwicklungswoche dann immer noch ein konkret anstehendes Serendipity-Projekt haben werde. Andererseits ist das Podcast-Plugin ganz spannend und ich glaube, dass es mit vertretbarem Aufwand in einen guten Zustand kommen kann. Morgen werde ich mich aber statt dem Plugin meinem Blog widmen, diese Installation auf die Alpha aktualisieren und schauen, ob es dort noch Probleme gibt.
Serendipity-Entwicklungswoche, Tag 5: Nochmal ein nochmal, diesmal XML_RPC und Podcasts
Wednesday, 13. February 2019
Wie gestern habe ich an diesem Tag der Entwicklungswoche mit den bundled-libs angefangen, ungleich gestern habe ich mit einem Entwurf für ein neues Plugin aufgehört.
XML_RPC und PHP 7.2 war doch noch kaputt
Warum auch immer ich vorher keine Fehlermeldung bekam, das XML_RPC-Modul hatte noch andere Inkompatibilitäten als nur den veralteten Konstruktor von vorgestern. An drei Stellen wurde noch das genauso veraltete each
benutzt. Leider ist die letzte Version upstream von 2011 und die Library mit einer anderen auszuwechseln wäre wesentlich mehr Arbeit als es das beim Cache war. Also habe ich einen Patch gebaut. Eine besser gepflegte Version wie phpxmlrpc einzubauen wäre schöner und ich hoffe, jemand anders macht das irgendwann. Mir reicht erstmal die Zeit dafür nicht.
Podcasts
Warum "nochmal Podcasts" wenn ich über Podcasts noch gar nicht sprach? Es ist ein lange geäußerter Wunsch von Dirk und anderen, der schon mehrmals an der Komplexität des Themas scheiterte. Wobei das Fehlen von podcastenden Entwicklern sicher auch nicht hilft, ich zum Beispiel habe nur wenig Ahnung was genau gebraucht wird.
Im Geiste der Woche habe ich diesmal trotzdem angefangen, so gut es eben geht. Herausgekommen ist das hier:
Man kann sich das auch anhören, ich habe vor dem Schreiben des Plugins einen Mini-Podcast über meine Ideen für dieses Plugin aufgenommen.
Was man im Screenshot nicht sieht ist das enclosure im RSS-Feed, aber das ist da. Ebenfalls nicht zu sehen ist etwas weniger erfreulicheres: Das Plugin hat ein konzeptionelles Problem. Weil die Mediendatenbank Audio-Dateien ohne ID in Artikel einfügt fehlt diese ID dem Plugin. Deswegen kann es bisher nur mit der URL arbeiten, es kann noch nicht die Audiodatei auf dem Server direkt anschauen. Aber wir können ja jetzt besprechen ob das wirklich nötig ist, dann würde ich morgen versuchen das im Kern umzubauen.
Wer das Plugin testen will kann sich den Quellcode auf Github anschauen und herunterladen. Es ist definitiv noch nicht fertig, aber wenn es fertig werden soll braucht es Feedback von Podcastern.
Serendipity-Entwicklungswoche, Tag 4: Über die Bundled-Libs zu Smarty und nochmal zum Cache
Tuesday, 12. February 2019
Heute war mein Einsatz ähnlich kurz wie gestern, aber gefühlt was es bisher einer der produktivsten Tage der Entwicklungswoche. Ich kam nämlich zum letzten Punkt meiner Agenda und konnte dort völlig überraschenderweise ein tolles neues Feature einbauen.
Smarty Pflichtupgrade
Aber erstmal zu Smarty. Im Forum hatte chris_goe eine seltsame Datei gemeldet, eine Verknüpfung, die upstream in das damalige Smarty-Release gerutscht war. Seitdem gab es sowieso neue und sogar Sicherheitsupdates beinhaltende neue Versionen. Also löschte ich das gesamte Smart-Verzeichnis und ersetzt es mit einem neuen Release, diesmal auch darauf achtend den Demo-Ordner zu entfernen. Der Commit ist leider unlesbar riesig, weil an irgendeiner Stelle automatisiert die Codeformatierung verändert wurde (ob bei uns im Code oder auf Seite von Smarty werde ich zwecks Ärgervermeidung nicht recherchieren). Immerhin funktioniert s9y mit der neuen Version weiterhin und scheinbar problemlos.
Ein besserer Cache
Smarty zu aktualisieren war Pflicht, nochmal den Cache anzugehen war eine Freude. Eigentlich wollte ich nur Cache/Lite ebenfalls aktualisieren, stolperte dabei aber über voku/simple-cache. Das ist ein mehrere Systeme unterstützender Cache-Layer: Tolle Optionen sind memcached und Redis. Und als Fallback dient OpCache, bei dem ähnlich wie mit Cache/Lite eine Datei erstellt wird. Gut, dazu gibt es nochmal ein Fallback, ein für uns unnützes nicht-persistentes Speichern im Array, aber OpCache sollte überall an sein.
Erst dachte ich "cool, aber wir haben ja schon Cache/Lite und das Projekt sieht etwas zu klein aus". Dann sah ich das:
Wir brauchen sowieso möglichst bald eine neue Cache-Lösung. Warum nicht jetzt? Also durfte voku/simple-cache per composer in die bundled-libs und ich stellte direkt den Code des internen Cache um (der übrigens bei den wenigen Plugins der Standardinstallation auf uberspace mit MySQL wenig bringt, dort ist die Datenbank schnell genug).
Eine aktiv gepflegte Cache-Library zur Hand zu haben ist nicht der einzige Nutzen. Sondern wir kriegen kostenlos die Funktion, Sachen in Memcached und Redis und damit im Arbeitsspeicher zu cachen. Das ist viel schneller als Speichern auf der Platte, egal ob in einer Datei oder Datenbank. Und besonderes interessant für mich: Es ist besonders attraktiv auf Scaleway-Servern und ähnlich langsamen Systemen mit viel verfügbarem Arbeitsspeicher.
Wieviel das bringt muss ich noch testen.
Damit habe ich alle im Vorhinein geplanten Themen der Woche durch. Robert hat sich einen Blick auf die XMLRPC-Schnittstelle für die Editoren gewünscht, davon habe ich aber keine Ahnung und würde vielleicht etwas anderes vorziehen.
Gibt es denn sonst noch Wünsche für die letzten drei Tage?