Blog: Latest Entries (15):


Fritzbox VPN unter Linux

Mit dem doch sehr kompliziert einzustellen openVPN von Linux kann sich leider nicht mit einer Fritzbox verbinden. Dafür braucht man sehr simple Konsolen Programm vpnc, das auch besonders gut ist, wenn man mehrere VPN-Verbindungen managen muss, da man für jede Verbindung eine eigene Conf-Datei anlegen kann und diese auch ohne Probleme zwischen Systemen hin und her kopieren kann.

Erstmal braucht man vpnc


sudo apt-get install vpnc


Danach muss eine Config-Datei mit den Verbindungsdaten angelegt werden:

nano ~/vpn.conf


mit dem Inhalt:

IPSec gateway _myfritz_url_without_port_
IPSec ID _vpn_username_
IPSec secret _ipsec_secret_
Xauth username _vpn_username_
Xauth passwort _vpn_password_


Alle Daten bekommt man nach dem Anlegen eines VPN-Benutzers in der Fritz direkt angezeigt und man sie sich in der Benutzerverwaltung immer wieder anzeigen lassen.

bbcode-image


Der Verbindungsaufbau geschieht auch über die Konsole:

sudo vpnc ~/vpn.conf


Nun befindet man sich, wenn die Zugangsdaten richtig waren, im Netz der Fritzbox und kann auf alles im Netzwerk wie ein NAS oder MotionEye zugreifen.

Das Trennen der Verbindung geht auch sehr einfach:

sudo vpnc-disconnect

Shopware 6: Units zuerst anlegen!

Ich habe gerade die Early Access Version von Shopware 6 installiert und erst einmal einen Hersteller und einen eigenen Artikel angelegt.

Das Problem war, dass ich keine Scale-Unit mit angeben konnte, da ich einfach noch keine angelegt hatte und nicht alle Artikeldaten nochmal neu eingeben wollte habe ich das Feld einfach leer gelassen. Speichern klappte ohne Probleme.

bbcode-image


Die Kategorie funktionierte dann aber nicht mehr.

bbcode-image


Zum Glück war mir das ja direkt aufgefallen, dass ich da was nicht eingeben konnte und habe erst einmal versucht eine Unit anzulegen und zu ergänzen.

bbcode-image


Danach funktionierte es dann auch alles.

bbcode-image


Da muss man also aufpassen, weil fehlende Eingaben noch viel kaputt machen können. Hab da auch gleich ein Issue dafür aufgemacht.

Shopware: ES Config

Hier einmal eine Default-Config für ElasticSearch unter Shopware 5. Gerade mit der Composer-Installation ist es sehr praktisch alle benötigten Felder direkt mal zu sehen.


'shopware.es' => array(
'prefix' => 'sw_shop',
'enabled' => false,
'write_backlog' => true,
'number_of_replicas' => NULL,
'number_of_shards' => NULL,
'total_fields_limit' => NULL,
'max_result_window' => 10000,
'wait_for_status' => 'green',
'dynamic_mapping_enabled' => true,
'batchsize' => 500,
'backend' => array(
'batch_size' => 500,
'write_backlog' => false,
'enabled' => false,
),
'client' => array(
'hosts' => array(
0 => 'localhost:9200',
),
),
'logger' => array(
'level' => 400,
),
'max_expansions' => array(
'name' => 2,
'number' => 2,
),
'debug' => false,
),

Alienware vs neueres Lenovo Y520

Was hat sich eigentlich seit dem Kauf vom Alienware-Notebook (damals gebraucht für 750 EUR über Ebay-Kleinanzeigen) so getan.
Gaming-Notebooks sind meistens vergleichbar mit dem Mobile-Workstations des Herstellers, nur eben mit mehr RGB-LEDs. Das Alienware liefert immer noch gute Dienste bei Arbeit und Spielen. Es wurden nur mal die HDDs gegen eine SSD ausgetauscht.

Ich hab mit mal ein Lenovo Y520 als Vergleichsobjekt raus gesucht.

CPU: Beide haben ein i7 der damals vorletzten Generation.
RAM: Hier liegt das Alienware mit 16GB sogar vorne
GPU: GTX 1050 gegen eine nachgerüstete Quadro 3000K, da sehe ich die GTX 1050 in der gesamt Leistung + Stromverbrauch vorne
HDD/SSD: 128GB M.2 ist nicht viel, aber ok. Das Alienware war für die damalige Zeit auch gut ausgerüstet. Berücksichtig man die Zeit, liegen beide an sich gleich auf
Display: beide gleich, 15,6" halte ich sogar für etwas handlicher
Keyboard: DE gegen UK. Könnte man umbauen, aber ich habe mit UK/US Layouts nie schlechte Erfahrungen gemacht (schon seit Pentium Pro und WinNT-Zeiten)

Garantie: gibt es bei Ebay-Kleinanzeigen nicht.. deswegen musste die Quadro auch auf eigene Kosten nachgerüstet werden

Preis: 600 EUR ist da echt gut, dafür dass es von einem Händler kommt.

Im zeitlichen Kontext war das Alienware günstig und Lenovo Y520 (refurbished) noch günstiger... aber das Alienware funktioniert nicht mehr wie am ersten Tag, sondern durch SSD und Quadro sogar noch viel besser und macht einfach auch jedes Wetter mit.

bbcode-image


Update: Das hier ist gerade auch 600 EUR teuer und gleicht alle Defizite des ersten Models aus. Y520 bekommt man also schon sehr günstig in guten Konfigurationen.. muss nicht immer Alienware/Dell sein.

Vue: Animations einbauen

Mit Vue.js kann man relativ einfach und schnell normale CSS3-Animations mit den State-Übergängen von Elementen verknüpfen.


<transition name="dialogbox">
<div class="overlay-content card" v-if="textDialog.show">
<h4 class="text-center card-header">{{ textDialog.title }}</h4>
<div class="text-field card-body">
{{ textDialog.content }}
</div>
<div class="text-center card-footer">
<button v-on:click="closeDialogs" class="btn btn-dark">close</button>
</div>
</div>
</transition>


Durch die Transition-Tag wird das Element eingeschlossen, dessen Änderung mit der Animation verknüpft werden soll. Der Name ist der der erste Teil des Animationsnamens.


.dialogbox-enter-active {
animation: open 0.8s;
}

.dialogbox-leave-active {
animation: open 0.4s reverse;
}

@keyframes open {
0% {
opacity: 0;
transform: scale(0);
}
100% {
opacity: 1;
transform: scale(1);
}
}


die erste Klasse wird beim Anzeigen verwendet und die zweite, wenn das Element aus dem DOM entfernt wird.

Neben v.id geht auch v-show.

Lombok.. nie wieder ohne

Jeder Java-Entwickler kennt es. Properties definieren und dann Setter und Getter erzeugen lassen. Wenn man was ändert, wird es nervig und wenn man was hinzufügt, muss man die fehlenden Methoden neu generieren lassen. Das geht an sich immer schnell, ist aber doch immer nervig.

Lombok übernimmt die Erzeugung dieser Methoden zur Compiling-Zeit. Die IDE benötigt ein Plugin und der Rest wird dann über Maven erledigt.

bbcode-image



<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.4</version>
<scope>provided</scope>
</dependency>


Dadurch spart man Zeit und der Code wird sehr viel übersichtlicher.


package de.hannespries.globalstate;

import lombok.Data;

import java.util.Map;

@Data
public class Action {
private String token;
private String action;
private Map<String, Object> payload;

public Action(){

}

public Action(String action){
this.action = action;
}

public Action(String token, String action){
this.token = token;
this.action = action;
}
}

AngularJS + IE: OnChange mit Input und DataList

AngularJS hat beim IE (älter als Edge) ein Problem mit ng-change, wenn ein Input über eine DataList gefüllt wird. Dann wird einfach kein Event ausgelöst. Mit einer kleinen Hilf-Function und $scope.$applyAsync() in der Scope-Function kann man das aber reimplementieren.


function onChangeIE(value){
var ua = window.navigator.userAgent;
if((ua.indexOf('Trident/') > 0 || ua.indexOf('MSIE ') > 0) && ua.indexOf('Edge/') <= 0) {
angular.element(document.getElementById('my-ng-controller-element')).scope().myInputModel = value;
angular.element(document.getElementById('my-ng-controller-element')).scope().myNgChangeFunction();

}
}



<input type="text" list="thelist" oninput="onChangeIE(this.value)" id="input_pattern" ng-model="myInputModel" ng-change="myNgChangeFunction()"/>
<datalist id="thelist"></datalist>


Die beste Lösung wäre solche alten Browser nicht mehr zu supporten und dann vielleicht auch auf Vue.js oder eine aktuelle Angular-Version zu wechseln.

Aber für schnelles Prototypen, wo man wenig Code schreiben will, ist AngularJS noch immer sehr gut. Teilweise ist man so schnell damit, dass man während einer Diskussion die Ideen direkt nebenbei umsetzen und ausprobieren kann. Wenn man erst einmal viele Components schreiben muss, geht es nicht so gut, wie mit den AngularJS Templates und Dingen wie ng-options. Mit Vue.js ist man mit etwas Übung aber auch ähnlich schnell.

Docker: Fehlercode anpassen

Manchmal hat man einfach Scripts von anderen, die mit einem Fehlercode beendet werden und es ist vollkommen ok. Leider bricht bei so einem Fehler dann der Build-Process vom Docker-Image ab. Deswegen muss man den Fehlercode überschreiben.


RUN composer create-project shopware/composer-project my_shop --no-interaction --stability=dev; exit 0


Damit wird der Fehler wegen fehlender .env-Datei übergangen. Da man diese Datei an sich auch nicht braucht.

MinIO: Docker-Compose

Mit Docker-Compose kann man sich einfach eine MinIO-Instanz erzeugen und auch gleich benötigte Buckets erstellen, so dass man das Erzeugen der Buckets nicht im Code der Anwendung erledigen muss.


version: '3.3'

services:

minio:
image: minio/minio:RELEASE.2019-04-23T23-50-36Z
volumes:
- minio_data1:/data
ports:
- "9000:9000"
environment:
MINIO_ACCESS_KEY: minio
MINIO_SECRET_KEY: minioKey123
command: server /data

createbuckets:
image: minio/mc
depends_on:
- minio
entrypoint: >
/bin/sh -c "
/usr/bin/mc config host add myminio http://minio:9000 minio minioKey123;
/usr/bin/mc rm -r --force myminio/testbucket;
/usr/bin/mc mb myminio/testbucket;
/usr/bin/mc policy download myminio/testbucket;
exit 0;
"

volumes:
minio_data1:
driver: local

Docker: PHP Schnelltest

Falls man mal schnell ein PHP-Script testen will, kann man mit Docker-Compose das ganz einfach machen.


version: '3.3'

services:
http:
image: geerlingguy/php-apache:latest
ports:
- 8881:80
volumes:
- ./:/var/www/html:rw,delegated
command: ["/usr/sbin/apache2ctl", "-D", "FOREGROUND"]
restart: always


damit wird das aktuelle Verzeichnis unter localhost:8881 bereit gestellt.

Mit MySQL + Adminer (anstelle von Phpmyadmin) sieht es so aus:


version: '3.3'

services:
mysql:
image: bitnami/mysql:latest
environment:
MYSQL_ROOT_PASSWORD: root
MYSQL_DATABASE: test
MYSQL_USER: app
MYSQL_PASSWORD: app
volumes:
- ./devops/sql:/docker-entrypoint-initdb.d
ports:
- 3336:3306

http:
image: geerlingguy/php-apache:latest
ports:
- 8881:80
volumes:
- ./:/var/www/html:rw,delegated
command: ["/usr/sbin/apache2ctl", "-D", "FOREGROUND"]
restart: always
depends_on:
- adminer

adminer:
image: adminer
restart: always
ports:
- 8882:8080
depends_on:
- mysql


wenn man sein SQL-Script unter ./devops/sql/ ablegt wird dieses automatisch in die Datenbank importiert.

Shopware 6: Plugin-Dev Schnellübersicht

Was ich zur Plugin-Entwicklung mit Shopware 6 nach dem ersten Kennen lernen bei der Shopware AG am 1.7. zu sagen habe:

bbcode-image


- Varianten sind nun Produkte mit Parent
- Ids in der Datenbank sind nun UUUIDs (ramsey/uuid) die binär in der DB gespeichert werden
- Ein Plugin definiert sich allein durch eine composer.json und den darin enthaltenen Typ shopware-platform-plugin (ersetzt die plugin.xml) und PSR-4 Namespace und Plugin-Class werden darin definiert
- die Metadaten zum Plugin sind in der composer.json unter extra
- Changelog ist jetzte die CHANGELOG.md Datei und mit ISO-Lang Postfixes erhält man die verschiedenen Sprachen (soll wohl mal durch den Store ausgelesen werden können)
- ./bin/console plugin:create --create-config MySW6Plugin erstellt einen ein komplettes Plugin Skeleton mit Config-XML und passender composer.json
- src/Resources/views/ wird automatisch registriert (wenn man seine Views wo anders liegen hat kann man getViewPath überschreiben)
- in der Plugin-Config kann man card-Tags verwenden, um Blöcke zu bilden und alles besser strukturieren zu können
- man kann eigene Felder und View in die Plugin-Config einbringen, in dem man vor eine Vue-Component registriert hat und diese dort als <component name="MyComponent"/> angibt
- Plugin installieren: ./bin/console plugin:install --activate --clear-cache MySW6Plugin öfters ist ein refresh der Liste nötig und cache:clear sichert den liebgewonnen Workflow bei der Entwicklung mit Shopware :-)
- Es gibt kein Doctrine-ORM mehr sondern die eigene DAL die noch DBAL verwendet
- DAL ist so etwas wie ein halb fertiges ORM wo noch die Abstraktion wie Annotations fehlt und man deswegen die Beschreibung und Umwandlungen selbst per Hand vornehmen muss, so wie auch das Anlegen der DB-Tables
- Es gibt dort noch viel zu verbessern, aber das Grundgerüst funktioniert da schon sehr gut. Zu verbessern wäre z.B.: new IdField('id', 'id') auch als new IdField('id') schreiben zu können wenn DB-Column und Class-Field gleich bezeichnet sind
- die Field-Definition-Methode wird nur selten aufgerufen, deswegen kann man hier sonst auch eigene Lösungen wie CamelCase-Converter oder Annotion-Parser selbst integrieren
- Es gibt momentan immer 3 Klassen: Definition, Plain Old PHP Object und Collection (über @Method der eigenen Collection wird so etwas wie Generics versucht abzubilden... in Java wäre es nur List<MyEntity> list = ....)
- Es wir automatisch ein Repository für jede Entity erzeugt, das man mit Hilfe der service.xml injecten kann
- CLI, Subscriber, etc werden weiterhin über die service.xml verwaltet und implementieren nur andere Interfaces (sonst hat sich dort kaum was verändert)
- Es gibt keine Hooks mehr sondern nur noch Service-Decoration (wie die meisten wohl schon jetzt lieber verwendet haben) und eben die Events
- Es gibt mit der DAL ein Versions-Feld
- Änderungen an der Datenbank sollten über das Migration-System (Timestamp basiert) erledigt werden und nicht über die Lifecycle-Methoden des Plugins
- Es gibt Updates und destructive Update, die die Kompatibilität der DB zu älteren Plugin-Versionen zerstören. Beides kann getrennt ausgeführt werden (2 Phasen Update für Cluster-nodes)
- Die DAL unterstützt native Translated-Fields
- Extensions von Entities werden serialisiert in der DB gespeichert (ALTER TABLE, Extension, Serializer-Klasse, etc). Es gibt keine Attributes mehr.
- Gespeichert wird immer per Array (von Entity-Arrays) und es wird nicht das Object zum Speichern übergeben
- upsert (update or insert) ist da das neue merge
- Der Cart hat nun LineItems, die wiederum Children haben können
- LineItems sind nun vollkommen unabhängig von Produkten, es gibt nur noch ein Type und Enrichment-Handler, die Namen und Preise für diesen Type an das Item ran dekorieren
- ... damit kann man verschiedene Bestell-Modi wie "kaufen" und "ausleihen" pro Produkt zusammen in einem Warenkorb abbilden
- Die API stellt automatisch generierte Swagger-Beschreibungen bereit
- Es gibt mehrere APIs so dass nicht nur Datenspflege bestrieben werden kann, sondern auch komplette Bestellprozesse damit abgebildet werden können (damit kann man Shopware als Backend-Lösung für In-App-Käufe oder POS-Systeme verwenden)
- Dadurch kann man Shopware auch headless verwenden (ohne Storefron und Admin, oder nur mit einem davon)
- ein Product aht ein SalesChannelProduct... man kann dort bestimmt auch eine Stock-Quantity hinzufügen und so einen Sales-Channel abhängigen Lagerbestand implementieren (Jeder POS auf einen Event oder in Disco könnte ein eigener Sales-Channel werden und dann kann man genau
monitoren wo Getränke oder Verbrauchsmaterial zu neige geht und reagieren bevor die Mitarbeiter überhaupt merkten, dass in 15min wohl die Strohhalme ausgehen werden)

Insgesamt macht Shopware 6 einen wirklich sauberen und durch dachten Eindruck. Nur die Migrations brauchen noch etwas und die DAL könnte gut einen weiteren Layer drum herum gebrauchen (ich wäre für Annotations an der Entity-Klasse)

bbcode-image

PHP Filesystem Abstraktion: Flysystem und S3

Oft will man ja beim Speichern von Dateien flexibel bleiben. Beim Entwickeln lokal speichern, im Produktivsystem in S3 und auf einem alten Test-System vielleicht noch auf einem FTP-Server oder in Redis.

Jeder hat schon mal so was geschrieben um wenigstens das native Filesystem und FTP zu abstrahieren, weil der FTP-Server nur vom produktiven System aus zu erreichen war oder so. Mein PHP Framework hat auch solche Klassen. Aber ich hatte keine Lust mehr diese noch mal anzupassen und eine brauchbare Struktur mit Factory und Interfaces dort endlich mal zu implementieren... was die Klassen (2006 geschrieben) an sich dringend nötig hätten.

Deswegen habe ich mich einige Sekunden im Internet umgeguckt und Flysystem gefunden. Das kann natives FS, FTP, S3, Redis, OneDrive, Azure File Storage.... also alles was man so braucht.

Ich habe es mal mit S3 getestet. Natürlich müsste man sich erst einmal eine brauchbare Factory schreiben, aber da kann man sicher in seinem System auf die alten Strukturen zurück greifen und muss dann nicht alles umbauen, wenn man auf Flysystem wechselt.

Flysystem installieren für PHP:

php composer.phar require league/flysystem
php composer.phar require league/flysystem-aws-s3-v3


Meine test.php:

<?php
require 'vendor/autoload.php';

use League\Flysystem\AwsS3v3\AwsS3Adapter;
use League\Flysystem\Filesystem;

if(file_exists('./testdata/down.png')){
unlink('./testdata/down.png');
}

$s3 = new Aws\S3\S3Client([
'version' => 'latest',
'region' => 'us-east-1',
'endpoint' => 'http://localhost:9080',
'use_path_style_endpoint' => true,
'credentials' => [
'key' => 'AKIAIOSFODNN7EXAMPLE',
'secret' => 'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY',
],
]);

//use headBucket for exists-check
try{
$s3->createBucket([
'Bucket' => 'testimagebucket'
]);
$s3->waitUntil('BucketExists', ['Bucket' => 'testimagebucket']);
}
catch(Exception $e){
echo $e->getMessage();
}

$adapter = new AwsS3Adapter($s3, 'testimagebucket');
$filesystem = new Filesystem($adapter);

try{
$filesystem->writeStream('image01', fopen('./testdata/test_01.png', 'r'));
echo "Update\n";
file_put_contents('./testdata/down.png', $filesystem->readStream('image01'));
echo "Download\n";
$filesystem->delete('image01');
echo "Delete\n";
}
catch(Exception $e){
echo $e->getMessage();
}

try{
$s3->deleteBucket(['Bucket' => 'testimagebucket']);
}
catch(Exception $e){
echo $e->getMessage();
}


Mit dem Local-Adapter kann man das dann schnell für eine lokale Speicherung umbauen.

Shopware 6: Installation mit Docker unter Linux und Windows

Meine kurze Anleitung (Ubuntu 18.04):

Rechte setzen:

sudo chown -R $(whoami) ~/.npm

sudo chown -R $(whoami) ~/.composer

sudo chown -R $(whoami) ~/_project_folder_

NPM aktualisieren (wenn nötig):

sudo npm install npm@latest -g

Starten:

./psh.phar docker start AKA docker-compose up --build -d

./psh.phar docker:ssh AKA docker exec -it <insert_id_here> bash

innerhalb des Containers dann:

./psh.phar install

Danach sollte auf localhost:8000 der Shop starten.. wenn eine MySQL Auth.-Method Fehlermeldung kommt hilft die Container zu beenden. In der Docker-Compose das DB-Image von dem MySQL-Image auf das MariaDB-Image zu ändern und noch mal die Container neu zustarten und ein wieder install durchführen.

Dann sollte alles laufen und man kann sich unter localhost:8000/admin mit admin - shopware anmelden.

Wenn man die lokalen psh.phar-Aufrufe durch die direkten Docker-Befehle ersetzt, sollte es auch unter Windows funktionieren. Innerhalb des Containers kann man natürlich wieder psh.phar verwenden.

Update:
manchaml kann auch helfen, dieses vorher einmal auszuführen, bevor man den Container startet und dort install aufruft:


php composer update --ignore-platform-reqs

Amazon S3 / MinIO: Dateiupload und Download

Mal ein kleines Cloud- und Cluster-Thema.

Amazon S3 ist ein einfacher Key-Value Store, wo man auch sehr große Daten unterbringen kann. Gerade wenn man Dateien nicht auf dem selben Server speichern möchte oder ein Cluster betreiben will, ist S3 eine gute Alternative zu FTP oder NFS-Laufwerken. Der Server ist über HTTP zu erreichen und es gibt für alle möglichen Sprachen Clients. Eine S3 kompatible Implementierung ist MinIO. Ich hab hier ein kleines Beispiel gebaut, wo ich ein Bild hochlade und wieder downloade und danach alles auch wieder aufräume. Metadata habe ich auch genutzt. Wenn man komplexere Anwendungen hat wird man wohl eher nicht SaveAs verwenden sondern den mitgelieferten Stream aus dem Array verwenden.

Das Minio Docker-Image:

sudo docker pull minio/minio
sudo docker run -p 9080:9000 -e "MINIO_ACCESS_KEY=AKIAIOSFODNN7EXAMPLE" -e "MINIO_SECRET_KEY=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY" minio/minio server /data


Der S3-Client für PHP:

php composer.phar require aws/aws-sdk-php



meine test.php:

<?php
require 'vendor/autoload.php';

if(file_exists('./testdata/down.png')){
unlink('./testdata/down.png');
}

$s3 = new Aws\S3\S3Client([
'version' => 'latest',
'region' => 'us-east-1',
'endpoint' => 'http://localhost:9080',
'use_path_style_endpoint' => true,
'credentials' => [
'key' => 'AKIAIOSFODNN7EXAMPLE',
'secret' => 'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY',
],
]);

try{
$s3->createBucket([
'Bucket' => 'testimagebucket'
]);
$s3->waitUntil('BucketExists', ['Bucket' => 'testimagebucket']);
}
catch(Exception $e){
echo $e->getMessage();
}

$insert = $s3->putObject([
'Bucket' => 'testimagebucket',
'Key' => 'image01',
'SourceFile' => './testdata/test_01.png',
'Metadata' => [
'Filename' => 'test_01.png',
],
]);

echo "Uploaded the image\n";

$retrive = $s3->getObject([
'Bucket' => 'testimagebucket',
'Key' => 'image01',
'SaveAs' => './testdata/down.png'
]);

echo "Saved " . $retrive['Metadata']['Filename'] . " to down.png\n";

try{
$s3->deleteObject([
'Bucket' => 'testimagebucket',
'Key' => 'image01',
]);
$s3->deleteBucket(['Bucket' => 'testimagebucket']);
}
catch(Exception $e){
echo $e->getMessage();
}

PHP: File-Ext und Mime

Manchmal braucht man einfach schnell ein Mapping zwischen Dateierweiterung und Mime-Type. Besonders wenn man beliebige Dateien über eine Schnittstelle zum Download anbietet und diese aus einen FileStorage kommen oder diese dynamisch erzeugt werden. Für PHP gibt es Mimey, das einen da schnell und einfach hilft.

bbcode-image


Sehr praktisch... durch ältere Projekte, die so etwas selber machen, sollte man sich wirklich mal durch kämpfen und dadurch ersetzen.

Older posts:

Möchtest Du AdSense-Werbung erlauben und mir damit helfen die laufenden Kosten des Blogs tragen zu können?