Bonusové cvičení – Databáze řízená emailem (velký příklad)

MSD – Moje Skvělá Databáze

Zadání ve formě PDF

Úkolem je napsat databázi MSD – Moje Skvělá Databáze, která si bude pamatovat emailové adresy, jména a popisky osob a bude řízená přes emailové příkazy.

Disclaimer: Jedná se o starý lehce upravený, ale jinak originální a nefalšovaný Forstův příklad ze zkoušky v červnu 2012 :-)

Struktura databáze

Databáze by si měla pamatovat následující položky:

Jak bude databáze uložené je čistě na vás.


Ovládání

Databáze se bude ovládat příkazy přes email. Váš skript bude zavolán tak, že mu na vstupu bude předán celý email včetně hlaviček, a on na něj musí odpovídajícím způsobem zareagovat a odeslat výsledek operace emailem nazpět.

Z hlavičky emailu nás zajímá pouze hlavička From:, která obsahuje emailovou adresu odesílatele emailu. V těle každého emailu (tedy za prvním prázdným řádkem) pak bude právě jeden MSD příkaz (ale před i za MSD příkazem může být nějaký balast, třeba podpis nebo jiný text).

MSD příkaz bude zapsaný jako MSD <příkaz> <parametr> na samostatném řádku (příkaz je uvedený vždy, parametr když ho příkaz potřebuje). Pod příkazem pak mohou být zapsané jednotlivé klíče, viz následující ukázka:


From: emailova@adresa.cz
Subject: Nějaký subject, nezajímá nás
Hlavička: Jiná nezajímavá hlavička

Balast okolo, který nás nezajímá

MSD ADD
:NAME Tomáš Novák
:EMAIL tomas@novak.cz
:PHONE 777666555
:DESCR Nějaký slovní popis, který může být
 i víceřádkový, dokud jsou navazující řádky uvozeny mezerou.
 Takže tento řádek do popisu ještě patří.
Ale tento už do popisu nepatří a je to zase nějaký balast.

Pokud pod příkazem není zapsaný klíč EMAIL, tak se vezme email odesílatele. Zbylé položky (NAME, PHONE a DESCR) jsou nepovinné. Někde se ještě může vyskytnout položka KEY.


Podporované příkazy


Bonusové věci


Odesílání emailů

Odesílání emailů je velmi jednoduché, stačí k tomu použít příkaz mail a předat mu na standardním vstupu tělo emailu (hlavičky si vytvoří sám):

echo "Moje zprava" | mail -s "Predmet" nejaky@adresat.cz

Ukázkové řešení

Pro řešení jsem zvolil formát databáze takový, že v jednom CSV souboru si pamatuji všechny hodnoty každého záznamu s výjimkou popisu, který si ukládám do samostatného souboru pojmenovaného podle emailu (email neobsahuje znak / a jiné nám starosti nedělají).

Řešení je členěno do dvou skriptů. Bashový skript msd.sh si zavolá AWK skript parser.awk, aby naparsoval email a vrátil na výstupu všechny informace v domluveném formátu a pořadí (na každý řádek jednu). Toto si shellový skript načte a pak pomocí série podmínek if rozhodne, co má dělat.

Práce s databází byla zabalena do volání funkcí, které (zne)užívají, že mají přístup ke globálním proměnným ve skriptu a jsou to vlastně jen jakési zabalené a pojmenované kusy kódu. Jediná specialita je, že update provádím smazáním a opětovným vložením celého záznamu, je to na podobné úlohy zdaleka nejlepší způsob.

Pro lepší testování jsem obsahy emailů jenom vypisoval a neposílal je, kdybych tak chtěl učinit, jenom za echo přidám rouru do mail :-)

AWK parsovátko

Ke stažení: parser.awk

#!/usr/bin/awk -f

# 1. HLAVIČKA
# Pokud v hlavičce potkáme From, uložíme si ho
/^From: / && !telo {
	email = $2
}
# Prázdný řádek nás přepne do těla
/^$/ { telo = 1 }

# 2. ZAPNUTÍ A VYPNUTÍ ZPRACOVÁNÍ PŘÍKAZU
# Pokud jsme při zpracování příkazu potkali první řádku, která není příkazem,
# můžeme skončit (END sekce se provede)
v_prikazu && /^[^ :]/ { exit }

# Začátek příkazu a přepnutí do stavu "v_prikazu"
/^MSD/ && telo {
	# Jsme v příkazu
	v_prikazu = 1
	prikaz = $2
	# Pokud je to FIND, tak nás zajímá i další parametr
	# (TRIK) zajímá nás vše do konce řádky, tak jen vynulujeme první a druhý
	# parametr a zbytek si uložíme
	$1 = $2 = ""
	parametr = $0
}

# 3. NAČÍTÁNÍ POLÍ
# Použijeme podobný trik, jako se zpracováním třetího parametru u MSD - zrušíme
# první parametr a uložíme zbytek. U některých parametrů sice nedává smysl, aby
# byly víceslovné (třeba email), ale uděláme pro jednoduchost všechny stejně.

# Popis je speciální, přepne nás do dalšího módu
/^ / && v_popisu {
	popis = popis "\n" $0
}
# Pokud popis skončil, vyskočíme z módu
/^[^ ]/ && v_popisu {
	v_popisu = 0
}
/^:DESCR/ && v_prikazu {
	$1 = ""
	popis = $0
	v_popisu = 1
}

# Normální políčka
/^:NAME/ && v_prikazu {
	$1 = ""
	name = $0
}
/^:EMAIL/ && v_prikazu {
	$1 = ""
	email = $0
}
/^:PHONE/ && v_prikazu {
	$1 = ""
	phone = $0
}
/^:KEY/ && v_prikazu {
	$1 = ""
	key = $0
}

# 4. VYPSÁNÍ VÝSTUPU
# Vypíšeme vše na samostatné řádky. To, že nám někde přebývají mezery na začátku,
# nám vzhledem k načtení parametrů pomocí `read` nevadí (ten jí osekne) :-)
END {
	print prikaz
	print parametr
	print email
	print name
	print phone
	print key
	print popis
}

Shellový skript

Ke stažení: msd.sh

#!/bin/sh

# Popisy se budou ukládat do souborů DBDIR/<email>
DB="msd.db"
DBDIR="msd"
LOCKFILE="msd.lock"

# Zámek bránící vícenásobnému spuštění skriptu, ostatní čekají
while [ -f "$LOCKFILE" ]; do
	sleep 1
done
touch "$LOCKFILE"

saveToDB(){
	# Využijeme globálních proměnných a zapíšeme vše mimo popisu do základního
	# souboru
	echo "$email;$name;$phone;$key" >> "$DB"
	# A popis uložíme do souboru v DBDIR
	# (soubor můžeme pojmenovat přímo emailem, ten nemůže obsahovat /)
	echo "$popis" > "$DBDIR/$email"
}

deleteFromDB(){
	# Vymaže záznam(y) odpovídající proměnné email
	# a to tak, že do pomocného souboru vygrepuje jen ty záznamy, které
	# nezačínají daným emailem
	grep -v "^$email;" <"$DB" >"$DB.tmp"
	mv "$DB.tmp" "$DB"
}

updateDB(){
	# Update je výmaz + vložení
	deleteFromDB
	saveToDB
}

isInDB(){
	grep "^$email;" <"$DB" >/dev/null || return 1
}

loadFromDB(){
	# Načte z databáze řádek odpovídající emailu a vyřeže si z něj sloupce
	db_name=`grep "^$email;" < "$DB" | cut -d";" -f2`
	db_phone=`grep "^$email;" < "$DB" | cut -d";" -f3`
	db_key=`grep "^$email;" < "$DB" | cut -d";" -f4`
	# Načte celý soubor do proměnné
	db_popis=`cat "$DBDIR/$email"`
}

vlozKlicDoDB(){
	# Defaultujeme všechno z databáze (abychom nic nezměnili)
	name="$db_name"
	phone="$db_phone"
	popis="$db_popis"
	# Vyrobíme si náhodný klíč, třeba 10 bytů z /dev/urandom, kterou proženeme base64
	key=`head -c10 /dev/urandom | base64`
	# A updatujeme v databázi
	updateDB
}

findInDB(){
	# Jako první zkusí najít řádek v databázi
	if grep "$regex" < "$DB"; then
		email=`grep "$regex" < "$DB" | cut -d";" -f2`
	else
		email=`cd "$DBDIR" && grep -l "$regex" *`
	fi
}

# Jako první spustíme AWK na naparsování, AWK nám vrátí všechny hodnoty řádek
# po řádku (některé hodnoty mohou být prázdné)
# Je důležité, že všechny příkazy jsou ve skupině za pipe z AWK, jinak by nastavení
# proměnných nemělo vliv
awk -f parser.awk | {
	# Všechno načteme:
	read prikaz
	read regex
	read email
	read name
	read phone
	read key
	read popis
	# Pokud by popis pokračoval na další řádky:
	while read popis_radek; do
		popis="$popis
$popis_radek"
	done

	if [ $prikaz = "ADD" ]; then
		if isInDB; then
			echo "Chyba: Email $email uz je v databazi"
		else
			saveToDB
			echo "Uspesne pridano do databaze"
		fi
	elif [ $prikaz = "UPDATE" ]; then
		if isInDB; then
			loadFromDB
			if [ -z "$key" ]; then
				echo "Vyžaduje autorizaci"
				# 1. Vypíšeme obsah emailu, dokud máme hodnoty co chtěl někdo updatovat
				# V popisu pro vypsání do emailu musíme na začátek každé řádky přidat mezeru
				popis=`echo "$popis" | sed 's/^/ /g;'`
				echo -e "\n\nMSD UPDATE\n:EMAIL $email\n:NAME $name\n:PHONE $phone\n:DESCR$popis"
				# 2. Vyrobíme klíč a vypíšeme ho taky
				vlozKlicDoDB
				echo ":KEY $key"
			elif [ "$key" = "$db_key" ]; then
				# 1. Defaultujeme nevyplněné hodnoty z databáze
				[ -z "$name" ] && name="$db_name"
				[ -z "$phone" ] && phone="$db_phone"
				[ -z "$popis" ] && popis="$db_popis"
				# 2. Provedeme update
				updateDB
				echo "Update proveden"
			else
				echo "Chyba: Nesprávný klíč"
			fi
		else
			echo "Chyba: Záznam s emailem $email není v databázi"
		fi
	elif [ $prikaz = "DEL" ]; then
		if isInDB; then
			loadFromDB
			if [ -z "$key" ]; then
				# Vyrobíme klíč a pošleme ho emailem
				echo "Vyžaduje autorizaci"
				vlozKlicDoDB
				echo -e "\n\nMSD DEL\n:EMAIL $email\n:KEY $key"
			elif [ "$key" = "$db_key" ]; then
				# Provedeme samotnou akci
				deleteFromDB
				echo "Vymazani provedeno"
			else
				echo "Chyba: Nesprávný klíč"
			fi
		else
			echo "Chyba: Záznam s emailem $email není v databázi"
		fi
	elif [ $prikaz = "FIND" ]; then
		email=""
		findInDB
		if [ -z "$email" ]; then
			echo "Nic nebylo nalezeno"
		else
			# Na základě emailu načteme údaje z databáze a vypíšeme je
			loadFromDB
			# V popisu pro vypsání do emailu musíme na začátek každé řádky přidat mezeru
			db_popis=`echo "$db_popis" | sed 's/^/ /g;'`
			echo ":EMAIL $email\n:NAME $db_name\n:PHONE $db_phone\n:DESCR$db_popis"
		fi
	fi
}

# Smazání zámku
rm "$LOCKFILE"