Přímé stažení souboru v NETTE pomocí redirectu formuláře

Potřeboval jsem udělat jednoduché stahovadlo souboru. Požadavky byly, aby pro stažení se muselo zadat heslo a aby soubor nešlo ani následně stahovat přes nějakou konstantní URL.

Níže uváděné řešení již považuji za zastaralé a nechávám ho tady jen aby bylo možné porovnat, že i toto jde v NETTE napsat lépe. Pokračujte zde…


Řešení je nakonec triviální:
Presenter:

< ?php class PcClientPresenter extends BasePresenter {
 	protected function createComponentForm($name) {
 		$form = new AppForm($this, $name);
 		$form->addPassword('heslo', 'Heslo: ')
			->addRule(Form::FILLED, 'Heslo musí být vyplněné.');
		$form->addSubmit('send', 'Stáhnout soubor »');
 
		$form->onSubmit[] = array($this, 'formSubmitted');
	}
 
	public function formSubmitted($form) {
		$file = WWW_DIR . '/data/soubor.zip'; // soubor může být úplně mimo web root (=nestáhnutelný pomocí URL)
		$fileName = 'Soubor po stažení.zip'; // název pod kterým se bude soubor stahovat uživateli
 
		$arr = $form->getValues();
		if ($arr['heslo'] === 'HESLO') {
			$httpResponse = Environment::getHttpResponse();
			// Pošleme prohlížeči hlavičky pomocí kterých mu řekneme že se jedná o stahování
			$httpResponse->setContentType('application/octet-stream');
			$httpResponse->setContentType('application/force-download');
			$httpResponse->setContentType('application/download');
			$httpResponse->setHeader('Pragma', "public");
			$httpResponse->setHeader('Expires', 0);
			$httpResponse->setHeader('Cache-Control', "must-revalidate, post-check=0, pre-check=0");
			$httpResponse->setHeader('Content-Transfer-Encoding', "binary");
			$httpResponse->setHeader('Content-Description', "File Transfer");
			$httpResponse->setHeader('Content-Length', filesize($file));
			$httpResponse->setHeader('Content-Disposition', 'attachment; filename="' . $fileName . '"');
 
			// v cyklu načítáme postupně soubor a posíláme ho prohlížeči (u malých souborů by to šlo najednou ale u velkých by došla dostupná paměť pro PHP)
			$fp = fopen($file, 'r');
			while (!feof($fp)) {
				echo fread($fp, 65536);
				flush();
			}
			fclose($fp);
			$this->terminate();
		} else {
			$this->flashMessage('Nesouhlasí zadané heslo.', 'error');
		}
	}
 
	public function actionDefault() {}
}

Template:

{block #content}
<h1>Stažení PC klienta</h1>
<div class="flash {$flash->type}">{$flash->message}</div>
{control form}
{/block}


Vše!

Posted in nette, php | Leave a comment

Jak se odkazovat na předchozí a následující příspěvek

V tomto příspěvku mi nejde o klasické stránkování, které se dá snadno vyřešit pomocí klauzule LIMIT v MySQL.
Nejde mi ani o seznamy, které se dají jednoduše a striktně seřadit podle velikosti (číslo, čas), ale jde mi o případ, kdy máme například seznam článků seřazených podle jména a chceme jednoduše přecházet na další či předchozí. Tady řazení podle operátorů <, > nebude to pravé.
Vyřešit se to dá následovně (vykopírováno z NETTE aplikace s použitou knihovnou DIBI):

// funkce zobrazuje objednávku dle ID, pokud ji pošleme parametr KAM, tak se zpracuje a funkce se zavolá znova jen s konkrétní ID
 
public function renderDetail($id, $kam = '') {
	if ($kam == '') {
		$this->template->rows = dibi::fetchAll('SELECT * FROM [objednavka_polozky] [o] JOIN [w_zbozi] USING ([kod_zbozi]) JOIN [objednavka_hlavicka] USING ([id_objednavka]) WHERE [id_objednavka] = %i GROUP BY [kod_zbozi]', $id);
	} elseif ($kam == 'PREV') {
		dibi::query('SET @poradi:=0, @poradinow:=0, @idprev:=0, @idnext:=0;');
		dibi::query('SELECT @poradi:=@poradi+1, IF(id_objednavka=%i, @poradinow:=@poradi, 0) FROM [objednavka_hlavicka] [oh] ORDER BY [cas] DESC', $id);
		dibi::query('SET @poradi:=0;');
		dibi::query('SELECT @poradi:=@poradi+1, IF(@poradinow-1=@poradi, @idprev:=id_objednavka, 0), IF(@poradinow+1=@poradi, @idnext:=id_objednavka, 0) FROM [objednavka_hlavicka] [oh] ORDER BY [cas] DESC');
		$row = dibi::fetch('SELECT @poradinow, @idnext AS next, @idprev AS prev;');
		if ($row['prev'] == 0) {
			$this->flashMessage('Nelze se již posunout na předchozí objednávku.', 'info');
			$this->redirect('Admin:detail', array('id' => $id));
		}
		$this->redirect('Admin:detail', array('id' => $row['prev']));
	} elseif ($kam == 'NEXT') {
		dibi::query('SET @poradi:=0, @poradinow:=0, @idprev:=0, @idnext:=0;');
		dibi::query('SELECT @poradi:=@poradi+1, IF(id_objednavka=%i, @poradinow:=@poradi, 0) FROM [objednavka_hlavicka] [oh] ORDER BY [cas] DESC', $id);
		dibi::query('SET @poradi:=0;');
		dibi::query('SELECT @poradi:=@poradi+1, IF(@poradinow-1=@poradi, @idprev:=id_objednavka, 0), IF(@poradinow+1=@poradi, @idnext:=id_objednavka, 0) FROM [objednavka_hlavicka] [oh] ORDER BY [cas] DESC');
		$row = dibi::fetch('SELECT @poradinow, @idnext AS next, @idprev AS prev;');
		if ($row['next'] == 0) {
			$this->flashMessage('Nelze se již posunout na následující objednávku.', 'info');
			$this->redirect('Admin:detail', array('id' => $id));
		}
		$this->redirect('Admin:detail', array('id' => $row['next']));
	} else {
		//chyba
		$this->redirect('Admin:objednavky');
	}
}

Příklad není úplně přehledný, protože jsem ho vykopíroval z hotové aplikace a aplikační logika není úplně čistá, ale pro demonstraci to doufám stačí.

Posted in nette, php | Leave a comment

Automatické dotahování údajů o podnikatelích z databáze ARES

Někde na internetu jsem viděl jak po zadání IČ se zbytek údajů o živnostníkovi/firmě dotáhl sám. A jelikož jsem si chtěl o tuto možnost rozšířit i své personální účetnictví, tak jsem pátral jak na to.

O databázi ARES jsem se dozvěděl již dříve, ale nikdy jsem se nedostal k samotné implementaci. Až dneska jsem narazil na článek Radka Hulána, řešící přesně toto a tak jsem si řekl, že se na to podívám.

Zdroj dat: Databáze ARES
Script: MyEGO blog

Script po úpravě na straně serveru (jedná se o NETTE akci):

public function handleLoadInfo($IC) {
	$this->payload->firma = array();
    	// dá se vybrat hned z několika zdrojů dle potřeby http://wwwinfo.mfcr.cz/ares/ares_xml.html.cz#k3
	define('ARES','http://wwwinfo.mfcr.cz/cgi-bin/ares/darv_bas.cgi?ico=');
	$ico = intval($IC);
	// nemohl jsem použít kvůli omezení na serveru, nahradil jsem pomocí CURL
	//$file = @file_get_contents(ARES.$ico);
	if ($curl = curl_init(ARES.$ico)) {
		curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
		$content = curl_exec($curl);
		//$info = curl_getinfo($curl);
		curl_close($curl);
		$xml = @simplexml_load_string($content);
	}
	$a = array();
	if (isset($xml)) {
		$ns = $xml->getDocNamespaces();
		$data = $xml->children($ns['are']);
		$el = $data->children($ns['D'])->VBAS;
		if (strval($el->ICO) == $ico) {
			$a['ico'] 	= strval($el->ICO);
			$a['dic'] 	= strval($el->DIC);
			$a['firma'] = strval($el->OF);
			$a['ulice']	= strval($el->AA->NU).' '.strval($el->AA->CD).'/'.strval($el->AA->CO);
			$a['mesto']	= strval($el->AA->N).'-'.strval($el->AA->NCO);
			$a['psc']	= strval($el->AA->PSC);
			$a['stav'] 	= 'ok';
		} else {
			$a['stav'] 	= 'IČ firmy nebylo nalezeno';
		}
	} else {
		$a['stav'] 	= 'Databáze ARES není dostupná';
	}
	$this->payload->firma = $a;
	$this->sendPayload();
}

Script po úpravě na straně uživatele:

<script type="text/javascript">
    <!--
	$('#frmkontakt-IC').change(function(event) {
		$.getJSON({link loadInfo!}, {'IC': $('#frmkontakt-IC').val()}, function(payload) {
			if (payload.firma.stav == 'ok') {
				$('input[name=DIC]').val(payload.firma.dic);
				$('input[name=prijmeni]').val(payload.firma.firma);
				$('input[name=ulice]').val(payload.firma.ulice);
				$('input[name=mesto]').val(payload.firma.mesto);
				$('input[name=psc]').val(payload.firma.psc);
			} else {
				alert(payload.firma.stav);
			}
		});
	});
	-->
</script>

Chtělo by to ještě doladit o podmínky kdy co a jak vypisovat, spojovat (číslo orientační a popisné, město a čtvrť, atd.), ale jako referenční implementace to je dobrý základ.

Posted in ajax, jQuery, js, linux, nette, php | Leave a comment

Zálohování databáze mysql a souborů na linuxu

Po delší době jsem se díky velikonocům dostal ke správě linuxového stroje. Již delší dobu jsem věděl, že by to chtělo nějak lépe vyřešit zálohování. Ne že by nefungovalo, ale řešení bylo poplatné době kdy vznikalo a také lidem, kteří ho implementovali.

A nakonec to nebylo ani tak těžké…

Záloha MySQL databází

Požadavky

  • denní zálohu držet 7 dní
  • týdenní zálohu držet 4 týdny
  • měsíční zálohu držet 3 měsíce
  • zálohovat každý web zvlášť
  • komprimovat zálohy

Použité prostředky

  • mysqldump
  • cron
  • scriptovací jazyk perl

Implementace

Script backup_day_mysql.pl

#!/usr/bin/perl -w
# vytáhneme si seznam jednotlivých databází, krom systémových
my @seznam = `echo "show databases" | mysql -uUSER_NAME -pPASSWORD | egrep -v '^(Database|information_schema|mysql)\$'`;
# připravíme si proměnou obsahující datum, pro pozdější název souboru
my $datum = `date +\%Y-\%m-\%d_\%H-\%M`;
$datum =~ s/\n//;
# každou databázi zkomprimujeme, pojmenujeme a uložíme do adresáře záloh
foreach my $radek (@seznam) {
  $radek =~ s/\n//;
  my $out = `mysqldump -uUSER_NAME -pPASSWORD $radek | bzip2 -c9 > /var/backups/day/mysql/$datum-$radek.bz2`;
}

Automatické spouštění
Toto zajistíme CRONem. CRON upravujeme po zadání příkazu crontab -e. Více v manuálu.

5 3 * * *  /home/backup_day_mysql.pl

Záloha souborů s weby

Požadavky

  • denní zálohu držet 7 dní
  • týdenní zálohu držet 4 týdny
  • měsíční zálohu držet 3 měsíce
  • zálohovat každý web zvlášť
  • komprimovat zálohy

Použité prostředky

  • cron
  • scriptovací jazyk perl

Implementace

Script backup_day.pl

#!/usr/bin/perl -w
my @seznam = `ls -1 /var/www`;
my $datum = `date +\%Y-\%m-\%d_\%H-\%M`;
$datum =~ s/\n//;
foreach my $radek (@seznam) {
  $radek =~ s/\n//;
  my $out = `tar -cvzf /var/backups/day/www/$datum-$radek.tgz /var/www/$radek`;
}

Automatické spouštění
Toto zajistíme opět CRONem.

10 3 * * * /home/backup_day.pl

Automatický úklid

Pokud bychom jen spouštěli tyto scripty, tak by velikost zabraného místa stále jen stoupala. A pokud provozujeme třeba web s 1GB daty, tak to máme každý den pěkný přírůstek.
Takže musíme po sobě i uklízet. To zajistíme pro denní zálohy dle požadavku příkazem:

# najdi všechny soubory starší než 7 dní a ty smaž
find /var/backups/day/www/ -type f -mtime +7 -delete

Opět spouštěným pravidelně v CRONu.

Pro denní, týdenní a měsíční zálohy to pak celé vypadá takto

CRON

# m h  dom mon dow   command
5 3 * * *  /home/backup_day_mysql.pl
10 3 * * * /home/backup_day.pl
15 3 * * * find /var/backups/day/www/ -type f -mtime +7 -delete
15 3 * * * find /var/backups/day/mysql/ -type f -mtime +7 -delete
20 3 * * 7 /home/bacup_mysql.pl
25 3 * * 7 /home/backup_week.pl
30 3 * * 7 find /var/backups/week/www/ -type f -mtime +28 -delete
30 3 * * 7 find /var/backups/week/mysql/ -type f -mtime +28 -delete
35 3 1 * * /home/backup_month_mysql.pl
40 3 1 * * /home/backup_month.pl
45 3 1 * * find /var/backups/month/www/ -type f -mtime +90 -delete
45 3 1 * * find /var/backups/month/mysql/ -type f -mtime +90 -delete

Know-how, které jsem v článku pro zjednodušení zamlčel

  • Soubory ukládám do /var/backups/{DRUH_ZALOHY}/{CO_ZALOHUJI}/ (DRUH_ZALOHY – day, week, month; CO_ZALOHUJI – www, mysql). Netvrdím, že to je jedniné možné a správné řešení, ale umožňuje se mi celkem snadno ve struktuře orientovat
  • /var/backups mám namapované na samostatný oddíl (/dev/sda6) což brání, aby díky zálohování došlo místo na primárním disku a tím to shodilo systém (zálohy ale v tuto dobu nepoběží). Na toto narazilo i mnoho profesionálních hostingů.
  • Scriptům je třeba dát právo na spuštění (např. chmod +x /home/backup_day.pl)
  • CRON je třeba nastavit tak, aby se nejlépe jednotlivé probíhající zálohy neovlivňovaly a zbytečně se nesnižoval výkon systému.
  • I přes tento sytém je třeba občas kontrolovat, zda zálohy probíhají korektně.
  • Je dobré zálohy občas stáhnout a vypálit či uložit geograficky dále od serveru.

Co se dá ještě vylepšit?

  • Mailová notifikace o (ne)provedení zálohy.
  • Automatický upload záloh mimo server (FTP, SCP).
  • Tvorba jen inkrementálních záloh.
  • Test správného vytvoření souborů.
  • Optimalizace příkazu mysqldump o doprovodné atributy.
Posted in linux | 5 Comments

Chyba „e100/d101m_ucode.bin“ při instalaci linuxové distribuce Debian Squezze

Jedná se o chybu, kdy nějaký hardware vyžaduje nesvobodný firmware. Smůla je, když je tímto HW síťová karta a tudíž si instalátor sám není schopen potřebný (firmware-linux-nonfree) balíček stáhnout.

Ani google moc nepomohl v otázce, kde si ho stáhnout a stahovat 52 instalačních CD se mi fakt nechtělo…

Řešením je stáhnout instalační CD, které právě tento firmware-linux-nonfree balíček obsahuje. To se dá provést přímým linkem na který jsem se dostal odtud.

Po stažení a nabootování z tohoto CD mě již nic nestálo v cestě a instalace proběhla bez problému.

Posted in linux | Leave a comment