Bonusové cvičení – Databáze řízená emailem (velký příklad)
MSD – Moje Skvělá Databáze
Ú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.
Struktura databáze
Databáze by si měla pamatovat následující položky:
- Emailová adresa (primární unikátní klíč)
- Jméno (i víceslovné)
- Telefonní číslo
- Popis (i víceřádkový, databáze ho musí uchovat i se všemi speciálními znaky)
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
- MSD ADD: Zkontroluje, že zadaná adresa není v databázi a přidá ji (se všemi položkami, které v emailu byly -- takže třeba jméno nebo popis mohou být vynechané) a pošle emailem odesílateli krátké potvrzení. Pokud adresa již v databázi je, tak si odesílateli emailem postěžuje a nic nepřidá.
- MSD UPDATE: Provede update záznamu podle položek z emailu (tedy updatuje
ty položky, které v emailu jsou, zbylé nechá beze změny). Važaduje ale
autorizaci klíčem od vlastníka:
- Pokud emailový příkaz neobsahuje klíč $\rightarrow$ vyrobí náhodný klíč, uloží si ho do databáze a pošle ho emailem na adresu "vlastníka" záznamu (ideálně ve tvaru, kdy bude vlastníkovi emailu stačit dát jenom odpovědět na email, tedy ve tvaru s připraveným příkazem k autorizaci).
- Pokud email obsahuje klíč a klíč souhlasí $\rightarrow$ provede update a odešle odesílateli příkazu krátké potvrzení,
- Pokud email obsahuje klíč, ale ten je nevalidní $\rightarrow$ postěžuje si emailem odesílateli emailového příkazu.
- MSD DEL: Odstraní záznam daný emailem, vyžaduje stejnou autorizaci, jako update.
- MSD FIND výraz: Vyhledává v záznamech podle výrazu, ideálně zvolte nějaký regex. Vyhledávat by se mělo ve všech polích databáze. Odešle emailem nazpět všechny výsledky (vypsané ve formátu, jako při přidávání a pokud by výsledků bylo víc, tak oddělené třemi prázdnými řádky).
Bonusové věci
- Zamykání – buďte připraveni na to, že váš skript se může chtít spustit vícekrát v tu samou chvíli, implementujte nějaký jednoduchý zamykací mechanismus, ať se vždy zpracuje nejdříve jeden proces a ať ostatní mezitím čekají (nemusí být úplně atomický, stačí když bude I v praxi dostatečně atomický).
- Blacklist – V proměnné BLACKLIST v shellu může být (mezerou oddělený)
seznam jmen (částí emailu před
@
), který je zakázaný. Pokud dorazí email od někoho takového, tak ho bez odpovědi ignorujte.
Odesílání emailů
Odesílání emailů je velmi jednoduché, stačí k tomu použít příkazmail
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"