Jak pracujete se soubory

Upozornění: Tohle vlákno je hodně staré a informace nemusí být platné pro současné Nette.
crempa
Člen | 198
+
0
-

Ahoj,
jak řešíte klasický problém co záznam to jeden nahraný soubor z hlediska editace záznamu? Jako nejjednodušší řešení mě v případě pouze jednoho souboru (resp. statického počtu souborů) k jednomu záznamu přijde zobrazit v případě editace místo FILE inputu jen odkaz jednak ke stažení daného souboru tak i k jeho smazání. Problém je jak toto realizovat v nette formulářích, aniž bych musel vytvářet vlastní šablonu a v ní si vše vykreslovat ručně… (pokud máme velký formulář a pouze jeden soubor, tak by to byla hloupá práce navíc)

Napadlo mě jako řešení vycházející z nedávno diskutovaného spanu do formuláře jen by se míto toho dal odkaz, nevýhodou ale je, že já potřebuju odkazy dva (stažení a smazání souboru) a taky by se musel nechat nějak zmizet input pro upload.
Další řešení už je pak takové, že se soubor zobrazí pod formem samotným a input se nenechá renderovat…
Jak toto řešíte vy ve vašich formech? :-)

PS: možná ještě k tomu uploadu, po validaci formuláře lze obsah uploadovaného souboru získat (např. pro uložení do databáze) nejlépe takto:

<?php

 //$uploadedFile je instance HttpUploadedFile
 $file = file_get_contents($uploadedFile->getTemporaryFile())

?>

nebo to lze ještě lépe ?

Ola
Člen | 385
+
0
-

Pokud Ti jde o to získat jeho obsah tak to je tak jak píšeš, pokud ho potřebuješ nahrát tak:

	// složka uploads má práva 777
	$uploaded->move(WWW_DIR . "/uploads");

pokud víš, že se jedná o obrázek tak místo toho co si posílal stačí dát

	// s $image jde nyní pracovat jako s Nette\Image objektem
	$image = $uploaded->getImage();
crempa
Člen | 198
+
0
-

Diky, chtel jsem jen ujistit ze to nejde jeste nejak elegantneji nez pres ten file_get_contents()

Jinak k prvni casti meho prispevku, nasel jsem asi mozne reseni, vicemene se jedna o rozsireni postupu uvedeneho v tom postu na ktery linkuju…

<?php
//	...
	$div = Html::el("div")->class("uploaded-files");
	$div->create("a")->href($this->link($destination, $args))->setText("file0001.jpg");
	$div->create("a")->href($this->link($destination, $args))->setText("[smazat]");
	$div->create("br");
	$div->create("a")->href($this->link($destination, $args))->setText("file0002.jpg");
	$div->create("a")->href($this->link($destination, $args))->setText("[smazat]");
//	...


	//v pripade  ze chceme umoznit upload dalsiho souboru
	$form->addFile("file", "File:")->setOption("description", $div);
//NEBO
	//v pripade ze dalsi upload povolit nechceme ale input nechame zobrazeny
	$form->addFile("file", "File:")->setOption("description", $div)->setDisabled(TRUE);
//NEBO
	//v pripade ze chceme zobrazit pouze seznam uploadovanych souboru (disabled je tam proto, aby si nam input nekdo nezobrazil a nepouzil..)
	$form->addFile("file", "File:")->setOption("description", $div)->setDisabled(TRUE)->getControlPrototype()->style['display'] = 'none';


?>

… takze diky pmg za navedeni.. :-)

ps: je to jen nastrel, ale urcite by tak sli udelat i ruzne „kudrlinky“ okolo (jako i nahledy nahranych obrazku a tak.. ackoli to uz bych asi resil vlastnim renderem formulare)

ps: proc nejde (nebo jak na to) udelat Html::el("div")->class("uploaded-files")->add(Html::el("div")->class("uploaded-file")).... ?? tj. add do konkretniho elementu…

Editoval crempa (23. 2. 2009 20:37)

RaR
Člen | 42
+
0
-

Může mi někdo poradit jak přesně uploadnout soubor?
Když mám ve formuláři

<?php
$form->addFile('file', 'Cesta k souboru:')
	->addRule(Forms::FILLED, "Vyberte soubor")
	->addRule(Form::MIME_TYPE, 'Nepovolený typ souboru', $mimeTypes); //tím si nejsem jistý
;
?>

kde a jak přesunu soubor pomocí move(), kde a jak zkontroluji, jestli se soubori korektne uploadnul isOk(). Nemáte někdo kousek funkčního kódu o který by jste se podělili. Děkuji předem.

Jod
Člen | 701
+
0
-
$values = $form->getValues();
$values['file']; // <-- Vracia HttpUploadedFile

https://api.nette.org/…dedFile.html

Editoval Jod (9. 3. 2009 17:12)

Patrik Votoček
Člen | 2221
+
0
-

RaR napsal(a):

kde a jak přesunu soubor pomocí move(), kde a jak zkontroluji, jestli se soubori korektne uploadnul isOk(). Nemáte někdo kousek funkčního kódu o který by jste se podělili. Děkuji předem.

Určitě máš něco co ti zpracovává onen formulář.

<?php
public function editFormSubmitted($form)
{
	$file = $form['file']->getValue();
	//Pokud máš u file nastaveny FILLED tak tahle podmínka se provádí vrámci $form->isValid()
	if (empty($file))
	{
		...
	}
	elseif ($file->isOK())
	{
		$file->move(...);
	}
	else
	{
		$file->addError("Upload file was not successful");
	}
}
?>

Při psaní tohoto příspěvku jsem si pro jistotu ověřoval některá fakta ve zdorjových kódech nette. A myslím si že by nebylo naškodu při validaci formu kontrolovat zda upload souboru byl úspěšný pouze u FILLED ale i bez jakých koli pravidel. Je jasné že pouze v případě že se něják soubor odešle (pokud je empty tak ne).

Edit: Právě jsem dočetl https://forum.nette.org/…iewtopic.php?… a tak jsem isValid() odstranil.

Editoval vrtak-cz (9. 3. 2009 21:16)

David Grudl
Nette Core | 8082
+
0
-

vrtak-cz napsal(a):

Při psaní tohoto příspěvku jsem si pro jistotu ověřoval některá fakta ve zdorjových kódech nette. A myslím si že by nebylo naškodu při validaci formu kontrolovat zda upload souboru byl úspěšný pouze u FILLED ale i bez jakých koli pravidel. Je jasné že pouze v případě že se něják soubor odešle (pokud je empty tak ne).

To by stálo za zvážení.

RaR
Člen | 42
+
0
-

Díky moc, k něčemu podobnému jsem došel, je to ošetření je mnohem vychytanější.

<?php
$file->addError("Upload file was not successful");
?>

toto přidá chybu k formuláři, která se vypíše u něho a form se neodešle, nebo je třeba ji zobrazit jinak?

Ještě bych měl dotaz k String::webalize

<?php
$fileName = WWW_DIR . "/files/" . String::webalize($upload->getName());
$upload->move($fileName);
?>

1.Pokud použiji výse uvedenou konstrukci k odstranění nežádoucích znaků, tak můj.soubor.txt se převede na muj-soubor-txt. Pokud budu chtít příponu podechat aby byl výsledek muj-soubor.txt, musím ten název poskládat z kousků?

2.Jak zjistím, jestli právě uploadovaný soubor na daném místě již existuje?

3.Jak vytvořím odkaz na stažení souboru? Chtěl bych ho pouze stahovat, ne zobrazovat.

Jod
Člen | 701
+
0
-
$farr = explode('.', $upload->getName()); // 0 - file , 1 - txt
David Grudl
Nette Core | 8082
+
0
-

RaR napsal(a):

1.Pokud použiji výse uvedenou konstrukci k odstranění nežádoucích znaků, tak můj.soubor.txt se převede na muj-soubor-txt. Pokud budu chtít příponu podechat aby byl výsledek muj-soubor.txt, musím ten název poskládat z kousků?

tečku lze povolit String::webalize($name, '.'); Chce to pak ale z bezpečnostních důvodů ověřit, jestli se nevygeneroval název tvořený samýma tečkama.

2.Jak zjistím, jestli právě uploadovaný soubor na daném místě již existuje?

metoda move() případný existující soubor přemaže.

_Martin_
Generous Backer | 679
+
0
-

RaR napsal(a):

3.Jak vytvořím odkaz na stažení souboru? Chtěl bych ho pouze stahovat, ne zobrazovat.

Možná nějak nakonfigurovat web server (což netuším jak) a nebo si pomoci PHP. Pro stahování souborů používám následující kód (definovaný v BasePresenter.php):

/**
     * Show "download dialog" in browser for required file
     * @param  string
     * @param  string
     * @return void
     */
    final protected function getAttachment($namespace, $file)
    {
        try {
            $file = $this->storage->readFile($namespace, $file);

            $httpResponse = Environment::getHttpResponse();
            $httpResponse->setContentType('application/octetstream');
            $httpResponse->setHeader('Content-Description', 'File Transfer');
            $httpResponse->setHeader('Content-Disposition', 'attachment; filename="' . $file['fileName'] . '"');
            $httpResponse->setHeader('Content-Transfer-Encoding', 'binary');
            $httpResponse->setHeader('Expires', '0');
            $httpResponse->setHeader('Cache-Control', 'must-revalidate, post-check=0, pre-check=0');
            $httpResponse->setHeader('Pragma', 'public');
            $httpResponse->setHeader('Content-Length', $file['fileSize']);
            ob_clean();
            flush();
            readfile($file['realName']);

        } catch (InvalidStateException $e) {
            throw new BadRequestException($e->getMessage());
        }
        $this->terminate();
    }

Ona metoda je poté volána v konkrétních presenterech v metodě action (vhodně nadefinovanou routou):

public function actionGetFile($itemId, $file)
{
    $this->getAttachment('files/' . $itemId, $file);
}

$this->storage je můj vlastní objekt na správu souborů, ale určitě nebude problém si kód upravit k použití s tvým adresářem, třeba nějak takto:

$fileRequiredByUser = someSecurityCheckFunction($fileRequiredByUser);
$file = array(
	'fileName' => $fileRequiredByUser,
	'realName' => WWW_DIR . "/files/" . $fileRequiredByUser,
	'fileSize' => filesize(WWW_DIR . "/files/" . $fileRequiredByUser),
);
_Martin_
Generous Backer | 679
+
0
-

Třeba se to někomu bude hodit:

Ještě něco ke kontrole a zpracování nahrávaných souborů ve formulářích

Při zpracování nahrávání souborů jsem řešil několik problémů:

  • uživatelsky povolená max. velikost souboru vs. max. velikost souborou nastavená v PHP
  • mít možnost zjistit, zda uživatel soubor vyplnil či nikoliv (zde Form::FILLED nestačí)
  • informovat uživatele i o chybách ze strany PHP
  • oba poslední body zajistit již při definici formuláře

Protože formuláře umožňují definování vlastních validačních funkcí, řešení je vcelku jednoduché (příklady využívají kódu z mé vlastní knihovny, který najdete níže).

Jak tedy na to?

Nastavení maximální velikosti

Ještě před definicí formuláře postačí jednoduchý kód:

$maxFileSize = 1572864; // 1,5MB
$phpUploadMaxFileSize = FormTools::bytes(ini_get('upload_max_filesize'));
if ($phpUploadMaxFileSize < $maxFileSize) {
	$maxFileSize = $phpUploadMaxFileSize;
}

Snad jen krátce: Pokud je v PHP menší hodnota, použije se hodnota z PHP, jinak hodnota nastavená.
K čemu to? Abychom mohli uživatele správně informovat o maximální velikosti souboru.

Vylepšená kontrola nahraného souboru

Využijeme dvě metody:

  1. FormTools::fileUploaded
    • kontroluje, zda byl zadán nějaký soubor (nezávisle na tom, zda se jej podařilo nahrát)
    • jde o přímou náhradu Form::FILLED
  2. FormTools::uploadTest
    • kontroluje, zda byl soubor zadán a zda byl správně nahrán
    • generuje chybová hlášení

Použití:

$maxFileSize = ...; // viz. kód výše

// soubor je povinný
$form['requiredFile']
	->addCondition('FormTools::uploadTest', $maxFileSize)
		->addRule(Form::MIME_TYPE, 'Přílohou musí být obrázek formátu .jpg, .gif nebo .png.', 'image/jpeg,image/gif,image/png')
		->addRule(Form::MAX_FILE_SIZE, 'Velikost přílohy může být nanejvýš ' . TemplateHelpers::bytes($maxFileSize) . '.', $maxFileSize);

// soubor je volitelný
$form['otherFile']
	->addCondition('FormTools::fileUploaded')
		->addCondition('FormTools::uploadTest', $maxFileSize)
			->addRule(Form::MIME_TYPE, 'Přílohou musí být obrázek formátu .jpg, .gif nebo .png.', 'image/jpeg,image/gif,image/png')
			->addRule(Form::MAX_FILE_SIZE, 'Velikost přílohy může být nanejvýš ' . TemplateHelpers::bytes($maxFileSize) . '.', $maxFileSize);

Slibovaná vlastní knihovna

class FormTools extends Object
{

	static function fileUploaded(IFormControl $control)
	{
		$file = $control->value;

		return ($file instanceof HttpUploadedFile && $file->error !== UPLOAD_ERR_NO_FILE);
	}



	static function uploadTest(IFormControl $control, $maxFileSize)
	{
		$file = $control->value;

		if (!$file instanceof HttpUploadedFile) {
			$control->addError('Přílohu se nepodařilo nahrát.');
			return FALSE;

		} elseif ($file->isOk()) {
			return TRUE;

		} else {
			switch ($file->error) {
				case UPLOAD_ERR_INI_SIZE:
					$control->addError('Velikost přílohy může být nanejvýš ' . TemplateHelpers::bytes($maxFileSize) . '.');
					break;

				case UPLOAD_ERR_NO_FILE:
					$control->addError('Nevybrali jste žádný soubor.');
					break;

				default:
					$control->addError('Přílohu se nepodařilo nahrát.');
					break;
			}

			return FALSE;
		}

	}


	static public function bytes($val)
	{
		$val = trim($val);
		$last = strtolower($val[strlen($val)-1]);
		switch($last) {
			// The 'G' modifier is available since PHP 5.1.0
			case 'g':
				$val *= 1024;
			case 'm':
				$val *= 1024;
			case 'k':
				$val *= 1024;
		}

		return $val;
	}

}

Ovace či vlastní připomínky a návrhy jsou vítány =)

David Grudl
Nette Core | 8082
+
0
-

Ještě je tam jedna záludnost s nahráváním příliš velkých souborů. PHP na to umí zareagovat podivně a stránka se načte s prázdným (tuším) formulářem. Tohle by taky chtělo nějak postihnout.

pmg
Člen | 372
+
0
-

Pěkné, Martine. Co se týče uploadu, přidám odkaz na Yahoo UI Library: Uploader: i když jsem políčko pro upload souboru několikrát použil jako kodér, nikdy bych ho nechtěl používat jako uživatel – třeba pro upload fotografií na server. Tato knihovna při výběru souborů umožní označit více položek najednou, a nevyžaduje přitom zvláštní podporu ze strany serveru. Podobná věc pro MooTools.

Při posílání souborů přes PHP je ještě dobré myslet na kešování a podle možností odpovídat na hlavičky If-Modified-Since nebo ETag. V Nette můžeme s výhodou použít metodu Presenter::lastModified.

Server pak také umí soubory posílat s Transfer-Encoding: chunked nebo navázat přerušené stahování. Nasimulovat serverové chování by asi bylo dost složité.

_Martin_
Generous Backer | 679
+
0
-

David Grudl napsal(a):

Ještě je tam jedna záludnost s nahráváním příliš velkých souborů. PHP na to umí zareagovat podivně a stránka se načte s prázdným (tuším) formulářem. Tohle by taky chtělo nějak postihnout.

Pravda, pokud je soubor příliš velký a velikost celého požadavku je větší, než povolená (direktiva post_max_size), budou proměnné $_POST a $_FILES prázdné a formulář se bude tvářit, jako by nebyl odeslán. To by bylo lepší ošetřit přímo na úrovni samotného formuláře, neboť díky prázdné proměnné $_POST nemůžeme zjistit, co uživatel vyplnil a co nikoliv – chtělo by to tedy obecnější hlášku typu „Odeslaná data byla větší než XX, pravděpodobně jste se pokusili nahrát příliš velký soubor“.

Samozřejmě si to vyžádá změnu detekce odeslání – buď parametrem v adrese (?check=1) a nebo kontrolou velikosti POST požadavku

if ($_SERVER['CONTENT_LENGTH'] > FormTools::bytes(ini_get('post_max_size'))) {
	... // zpracování chyby
}

pmg napsal(a):

Vida, to kešování je taky chytrý nápad. Knihovny pro upload prozkoumám, vypadá to jako velmi dobré řešení uploadu fotek ve fotogalerii=)

crempa
Člen | 198
+
0
-

Kluci jak docilim ve formularich funcionality, kdy mam jedno pole soubor a druhe rekneme popis souboru a v pripade ze nekdo vyplni popis, ale ne soubor tak mu to vynada? Zkousel jsem neco jako

<?php
$form->addText('file_name', 'Popisek souboru:')
  ->addCondition(Form::FILLED)
  ->addRule(Form::LENGTH,'Popisek souboru musí být v rozmezí od %d do %d znaků', array(3, 20))
  ->addConditionOn($form['file'],Form::FILLED, true)
  	->addRule(~Form::FILLED,"bububu neneen");

?>

ale nezda se to funkcni

jinak diky _Martinovy_ za prispevek, osobne jsem si ho rozsiril jeste o kontrolu pripony souboru

(pripony resim interne v helperu, ale nebylo by problem to prepsat do config.ini a predavat jako dalsi parametr, btw: lze v config.ini nejak ulozit do variable i pole?)

<?php

  static function validateExtension(IFormControl $control)
  {
  	return in_array(strtolower(pathinfo($control->value->name, PATHINFO_EXTENSION)), self::get_allowed_extensions(true));
  }

  static function get_allowed_extensions($asArray = false)
  {
  	$extensions = array(
  		"jpeg",
  		"jpg",
  		"png",
  		"doc",
  		"pdf",
  		"zip",
  		"xls",
  		"ppt"
  	);

  	if(!$asArray)
  		$extensions = implode(",", $extensions);

  	return $extensions;
  }

?>

a pouziti pak treba..

<?php
$form->addFile("file", "Soubor:")
  ->addCondition('FormTools::fileUploaded')
  	->addCondition('FormTools::uploadTest', $maxFileSize)
  		->addRule('FormTools::validateExtension',"Nepovolená přípona souboru, systém podporuje pouze tyto přípony: (".FormTools::get_allowed_extensions().")")
  		->addRule(Form::MIME_TYPE, 'Neplatný typ souboru', FormTools::get_allowed_mimetypes())
  		->addRule(Form::MAX_FILE_SIZE, 'Velikost přílohy může být nanejvýš ' . TemplateHelpers::bytes($maxFileSize) . '.', $maxFileSize);

?>

A mel bych jeden maly feature request: neslo by zadavat mime typy i jako pole pri pouziti MIME_TYPE ?
Koukal jsem do zdrojaku a resila by to asi jen jedna podminka (protoze se stejne ten string na pole prevadi…)

Editoval crempa (1. 4. 2009 17:18)

Tomik
Nette Evangelist | 485
+
0
-

crempa napsal(a):

btw: lze v config.ini nejak ulozit do variable i pole?

Ano. Lze. Viz 1463-rev-237-rozsireni-syntaxe-ini-souboru.

[production]
items[] = 10
items[] = 20
items[] = 30
David Grudl
Nette Core | 8082
+
0
-

crempa napsal(a):

A mel bych jeden maly feature request: neslo by zadavat mime typy i jako pole pri pouziti MIME_TYPE ?
Koukal jsem do zdrojaku a resila by to asi jen jedna podminka (protoze se stejne ten string na pole prevadi…)

To by šlo.

pjoter
Člen | 118
+
0
-

Dotaz: Jak hashovat název souboru ještě před uploadem ? uploadedfile má pouze metodu getName() ale setName už ne. Poradí prosím někdo jak na to ?

Ondřej Mirtes
Člen | 1536
+
0
-

A k čemu to potřebuješ? Pokud chceš mít ve výsledku na serveru soubor s nějak upraveným názvem, jednoduše si ho tak pojmenuj v parametru pro metodu move().

pjoter
Člen | 118
+
0
-

Ondro díky, začínáš se stávat mým osobním rádcem :), teď ještě řeším jak změnit velikost tím pádem asi move nepoužiju nebo dá se nějak vrátit ten image zpátky do uploadedfile?
zatím to mám takhle ale mám error Unsupported image type. hlásí to na ten poslední řádek.
ta foto_path = ‚images/profil-fotos/‘

<?php
			$image = $values['file']->getImage();
			$image->resize(108, 120);
			$image->save($this->profil_foto_path.sha1_file($values['file']));
?>
Ondřej Mirtes
Člen | 1536
+
0
-

Co dělá ta tvoje metoda s tečkou názvu? $values['file']) je objekt HttpUploadedFile a ty v ní počítáš asi s obrázkem…

pjoter
Člen | 118
+
0
-

prostě tohle mi funguje:

<?php
	$values['file']->move($this->profil_foto_path.sha1_file($values['file']));
?>

ale potřebuju změnit velikost toho obrázku a nemůžu přijít jak na to.

pjoter
Člen | 118
+
0
-

Ondřej Mirtes napsal(a):

Co dělá ta tvoje metoda s tečkou názvu?

to je promenna s cestou a za teckou je hashnuty nazev souboru

Ondřej Mirtes
Člen | 1536
+
0
-

Aha, to jsem nerozluštil, přišlo mi to divný :o)

Tak jestli tohle nefunguje (možná je to nějaký bug), tak si pak znova vytvoř ten obrázek Image::fromFile($cesta), zmenši ho a ulož ho.

BTW: Měl bys používat absolutní cesty (od rootu filesystému) – k tomu slouží konstanty APP_DIR, LIBS_DIR a WWW_DIR.

sin
Člen | 82
+
0
-

Použil jsem výše uvedený kod od __Martin__a ale mam probelem s ob_clean(). Kdyz to pustim na localhostu tak mi to na zacatek souboru pripojí hlášku:

<br />
<b>Notice</b>:  ob_clean() [<a href='ref.outcontrol'>ref.outcontrol</a>]: failed to delete buffer. No buffer to delete. in <b>/Users/sinacek/www/osmnactka/app/models/FilesModel.php</b> on line <b>52</b><br />

na serveru jsem to neměl možnost zkusit.

worsik
Člen | 40
+
0
-

Ahoj,

použil jsem Martinovu metodu getAttachment() s mírnou úpravou a na ostrém serveru mi nechce fungovat.
Na lokálu jede vše v pohodě, ale ostrý server hlásí „Soubor nenalezen“ pokaždé, když se směruju na action, která volá getAttachment().
Problém je popsán v jiném vlákně:

https://forum.nette.org/…oadu-souboru

Dokázal bys mi Martine pomoci?
Po jakém rozdílu v nastavení se mám dívat?

_Martin_
Generous Backer | 679
+
0
-

Nevím, napadá mě něco o citlivosti na malá a velká písmena na Linuxu… Ale jak radí v tom druhém vlákně – použij FileDownloader, ten je mnohem propracovanější.