Jak pracujete se soubory
- crempa
- Člen | 198
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
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
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
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
$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
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 | 8218
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
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.
- David Grudl
- Nette Core | 8218
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
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
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:
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
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 | 8218
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
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
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
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)
- David Grudl
- Nette Core | 8218
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.
- Ondřej Mirtes
- Člen | 1536
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
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
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…
- Ondřej Mirtes
- Člen | 1536
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
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
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?