Állományok kiszolgálása PHP-ból
Ezúttal is egy a webfejlesztésben mindennapos témáról lesz szó. Gyakori feladat, hogy egy létező, vagy éppen dinamikusan generált fájlt kell alkalmazásunkból kiszolgálni. Semmi gond, kiadom a megfelelő fejléceket (megjegyzem, már itt is lehetnek bonyodalmak), aztán kiirtom a tartalmat és csókolom. A következőkben látni fogjuk a helyzet nem ennyire egyszerű, megvizsgáljuk, hogy milyen lehetőségeink vannak egy szofisztikáltabb megoldás megvalósításához.
Mielőtt belekezdünk, kicsit nézzük meg, hogy milyen esetekben is van szükség ilyen jellegű megoldásokra.
Gyakori alkalmazási mód, hogy bizonyos állományok letöltését csak azonosított felhasználóknak szeretnénk engedélyezni. Ilyenkor ezeket nem tesszük közvetlenül elérhetővé a netről, csak PHP-n keresztül, ahol már elvégezhető könnyedén a letöltő személy azonosítása, esetlegesen jogosultságainak ellenőrzése.
Használhatjuk akkor is, ha nem szeretnénk, hogy tartalmainkra mások közvetlenül tudjanak linkelni. Ilyenkor PHP-ból meg tudjuk nézni például a
Jól jöhet, ha például szeretnénk a letöltésbe plusz funkciókat beépíteni, például letöltés befejezésének figyelése, sebességének befolyásolása, stb..A MIME fejlécekről már esett szó az Emailek felépítése – lépésről lépésre I. című cikkünkben. A
Például próbáljuk ki a következőt, adjunk ki egy képet (megfelelő fejléccel)
Most kezdjük el átnézni, hogy milyen gondok is adódhatnak a fenti scripttel kapcsolatban.
A kimeneti buffer kezelő függvények segítségével összegyűjtjük a futás során keletkezett hibaüzeneteket, és kiírjuk őket egy fájlba.
Mint láthatjuk ez esetben egy ciklusban folyamatosan olvassuk a fájl egy-egy kisebb darabját, majd ezt írjuk ki. A buffer méretét célszerű az adott lemez blokkméretének megfelelően beállítani: legyen annak egész számú többszöröse.
A ciklus folyamán megtehetjük azt is, hogy folyamatosan figyeljük, hogy adott idő alatt mennyi adat ment ki, és szükség esetén beiktatunk egy kis várakozást. Ezzel tudjuk szabályozni a letöltés sebességét.
A következőkben végignézzük, hogy mely fejléceknek van szerepük a cache-lésben.
Részei:
Amit fontos észrevennünk, hogy itt külön képesek vagyunk mgekülönböztetni a böngésző és a köztes cache-eket. Például egy felhasználó saját tartalmainak cache-elését nem érdemes engedélyeznünk egy köztes cache-ben, csak a saját böngészőjében.
Amikor a cache-nek validálnia kell egy oldalt, akkor elküldi a szervernek a válaszban kapott
Egy lehetséges megoldás lehet a következő:A kérésben küldött fejlécek egy részét a PHP közvetlenül elérhetővé teszi számukra a
Megkülönböztetünk erős és gyenge validátorokat. Az erős validátor megváltozik, ha a válasz egyetlen bitje is változik. Egy gyenge validátor csak akkor változik, ha a válasz jelentése változik meg. Erős validátorokra ott lehet például olyan cachek esetén van szükség, melyek képesek a válasz egy részletére történő kérés kiszolgálására. Ha ez nem szükséges, akkor egy gyenge validátor használatával lényegesen jobb lehet a cache teljesítménye.
Nem elrettentésnek szánom, csak szokjunk egy kis RFC szintaxist :-). Szépen sorban:
A kezdő byte pozíció a 0. Az utolsó byte pozíció (ha megadták) nagyobb kell legyen, mint a kezdő, és kisebb kell legyen, mint a teljes hossz. Ellenkező esetben a kliensnek a választ érvénytelennek kell tekintenie. Ha nem megfelelő a kérésben megadott tartomány, akkor 406-os státusz kódú (Requested range not satisfiable - Kért tartomány nem kielégíthető) választ kell adnunk, ilyenkor használhatjuk a
Például egy 1kb-os fájl első és utolsó 100 byte-jának kiszolgálásakor a fejléc így néz ki:
Ezt már értjük is ;-), gyors magyarázat: megadhatunk egy tartományt (
És végezetül lássunk egy nem teljesne kidolgozott kódot arra, hogy hogyan is tudunk egy ilyen kérést kiszolgálni. És ezt már magunktól később be tudjuk építeni saját, vagy a fentebb látott letoltő scriptbe. Elöljáróban, mint említettem bizonyos fejléceket nem tudunk aEz csak egy nagyon alap megoldás, érdemes lenne majd mindenféle letöltés vezérlő programmal letesztelni, biztosan lehet még rajta finomítani, hisz sose feledjük, hogy egy dolog a szabvány, és más a valós élet.
No hát ennyi lenne a mai adag, majd egyszer talán leírom, hogy hogy született meg ez a cikk, nem volt egyszerű, egész regényesre sikeredett (1-2 hónapnyi Barátok közt ízgalmával simán vetekedne...
■ Mielőtt belekezdünk, kicsit nézzük meg, hogy milyen esetekben is van szükség ilyen jellegű megoldásokra.
Gyakori alkalmazási mód, hogy bizonyos állományok letöltését csak azonosított felhasználóknak szeretnénk engedélyezni. Ilyenkor ezeket nem tesszük közvetlenül elérhetővé a netről, csak PHP-n keresztül, ahol már elvégezhető könnyedén a letöltő személy azonosítása, esetlegesen jogosultságainak ellenőrzése.
Használhatjuk akkor is, ha nem szeretnénk, hogy tartalmainkra mások közvetlenül tudjanak linkelni. Ilyenkor PHP-ból meg tudjuk nézni például a
HTTP-REFERER
értékét, és ennek fényében döntünk, hogy kiszolgáljuk a kérést, vagy nem.Jól jöhet, ha például szeretnénk a letöltésbe plusz funkciókat beépíteni, például letöltés befejezésének figyelése, sebességének befolyásolása, stb..
Alap megoldás
Először is lássunk egy alap megoldást.<?php
$disposition = "attachment"; // "attachment" vagy "inline"
$mimeType = "application/octet-stream";
$fileName = "letolt.zip";
$path = "/var/www-data/dl/$fileName";
// Egy kis Internet Explorer hack
// http://weblabor.hu/levlistak/wl-phplista/2003/03/027752
if (isset($_SERVER["HTTPS"])) {
header("Pragma: ");
}
header("Content-Type: $mimeType");
header("Content-Disposition:$disposition; filename=\"".trim(htmlentities($fileName))."\"");
header("Content-Description: ".trim(htmlentities($fileName)));
header("Content-Length: ".(string)(filesize($path)));
// Ez itt elvileg kell ahhoz, hogy bizonyos alkalmazások mentés
// nélkül meg tudják nyitni a letöltött állományt.
header("Cache-Control: must-revalidate, post-check=0, pre-check=0");
header("Connection: close");
readfile($path);
?>
Content-Type
részletes leírását megtaláljuk ott. A Content-Disposition
és a Content-Description
fejlécekkel leírhatjuk az adott tartalmat. Az előbbiben a szabvány szerint használhatjuk az attachment
és az inline
attribútumot. Előbbi azt jelzi, hogy a kiadott tartalmat mentésre szánjuk, míg utóbbi esetében a böngészőben való megnyitást szeretnénk kezdeményezni. Például egy tömörített fájlt jellemzően mentésre szánunk, viszont egy dinamikusan generált Excel fájl esetén kívánatos lehet, hogy egyből nyíljon meg a böngészőben. Mindkét fejlécben ezenkívül megadjuk a kiadott állomány nevét. Az azonban kliens függő, hogy mennyire veszi figyelembe ezeket a fejléceket.Például próbáljuk ki a következőt, adjunk ki egy képet (megfelelő fejléccel)
inline
és attachment
attribútummal is. Internet Explorer, Firefox és Opera esetén is azt fogjuk tapasztalni, hogy első esetben megjeleníti, második esetben letöltésre ajánlja fel a képet. Ezután változtassuk meg a kép kiterjesztését mondjuk zip
-re, és ismételjük meg az előzőt. Azt fogjuk tapasztalni, hogy Internet Explorer és Firefox alatt semmi sem változik, viszont Opera esetében inline
"módban" már nem jeleníti meg a képet, hanem letöltésre ajánlja fel. Ráadásul a zip kiterjesztést lecseréli a megadott MIME típusnak megfelelően, például kep.zip
helyett kep.jpg
lesz az elmentett fájl neve. De egyik nagy kedvencünk, a Wget, egyáltalán nem veszi figyelembe ezeket a fejléceket, így az elmentett fájl neve mondjuk download.php lesz.Most kezdjük el átnézni, hogy milyen gondok is adódhatnak a fenti scripttel kapcsolatban.
Debuggolás
Ezekben a fájlokban nehézkesebb a hibakeresés, hiszen böngészőn keresztül használva őket nem jelennek meg számunkra az esetleges hibaüzenetek. Ekkor lehet hasznos a következő kis apró ötlet.<?php
ob_start();
// ... fejléc beállítós móka...
$content = ob_get_contents();
ob_end_clean();
if ($content) {
$file = fopen('download.log', 'w');
fputs($file, $content);
fclose($file);
}
// ... fájl kipakolós móka...
?>
fpassthru
A PHP kézikönyv egyik felhasználói kommentje arra hívja fel figyelmünket, hogy areadfile
megváltoztatja a fájl utolsó módosításának dátumát. Ez okozhat némi galibát, és bár előfordulhat, hogy újabb verziókban már ez javítva van, mégiscsak szerencsésebb, ha igyekszünk védekezni ellene, főleg, hogy a hozzászólás egyből kezünkbe is adja a megoldást: használjuk helyette a fpassthru
függvényt. Ennek megfelelően a problémát okozó kódrészletet cseréljük le a következőre:
<?php
$fp = fopen($path,"rb");
fpassthru($fp);
fclose($fp);
?>
Memória használat
Mindkét előző megoldásnak van egy hátránya, hogy kiküldés előtt a teljes fájlt beolvassák. Ez nagy fájlok esetén komoly gondot okozhat, például párhuzamosan rácuppannak egy ilyen fájlra, akkor az igencsak komoly loadot eredményezhet a szerveren, folyamatos swappelésbe kényszerítheti. Szerencsére erre is van megoldás, a kérdéses helyen használjuk az alábbi kódot:
<?php
$bufferSize = 1024;
$file = fopen($path,"rb");
while(!feof($file)) {
echo fread($file, $bufferSize);
}
fclose($file);
?>
A ciklus folyamán megtehetjük azt is, hogy folyamatosan figyeljük, hogy adott idő alatt mennyi adat ment ki, és szükség esetén beiktatunk egy kis várakozást. Ezzel tudjuk szabályozni a letöltés sebességét.
Letöltés sikerességének megállapítása
A fentebb vázolt ciklikus kiszolgálás során szükség esetén loggolhatjuk azt is, hogy a felhasználó sikeresen letöltötte-e a fájlt vagy sem.<?php
ignore_user_abort(1);
set_time_limit(0);
// ... fejléc beállítós móka...
$fp = fopen($path)
while(!feof($fp)) {
print fread($fp,1024);
if(connection_aborted()) {
fclose($fp);
// Hibás letöltés loggolása
}
}
fclose($fp);
// Sikeres letöltés loggolása
?>
Cache-elés kezelése
Az adatok cache-elése hasznos a sávszélesség megtakarítására ismétlődő kérések esetén. Képek, hangok és egyéb, ritkán változó erőforrások (vagy komolyabb terhelés) esetén a tartalom lokálisan történő átmeneti tárolás lényegesen hatékonyabb, jobban preferált megoldás, mint az Interneten keresztül történő, ismétlődő letöltésük. A HTTP 1.1-es protokoll már kellően jó eszközöket nyújt számunkra a minél hatékonyabb cache-elési stratégiák kialakításának érdekében. Minél közelebb található a kívánt adat a felhasználó számítógépéhez, annál kevesebb adatnak kell összességében átutaznia a hálózaton, illetve mindezen adatok kiszolgálása kevesebb terhet jelent szerverünknek.A következőkben végignézzük, hogy mely fejléceknek van szerepük a cache-lésben.
Pragma
fejléc: általában minden példában látjuk, hogy ha azt szeretnénk elérni, hogy egy adott válasz ne kerüljön cache-elésre, akkor az egyik fontos dolog, hogy a válaszban helyezzük el a Pragma: no-cache
fejlécet. Az igazság az, hogy a szabvány nem igazán mond erről semmit, viszont mivel elég sokan hisznek benne (köztük jó pár proxy gyártó), így végül mégiscsak megéri hozzácsapni a válaszhoz. De túl nagy jelentőséget tulajdonítani neki nem érdemes.Expires
fejléc: azt az időpontot tartalmazza, ameddig az adott tartalom érvényes. Ezt követően a különböző fajta cache-knek ellenőrizniük kell a szerveren keresztül, hogy az érvényesség még mindig fent áll. A fejléc tartalma egy GMT dátum, melyet a PHP gmdate
függvényével tudunk generálni. Újabb okosság a kézikönyvből: ne használjuk a "T"
formázó karaktert, mert nem mindig a GMT
szöveget adja vissza, helyette használjuk a következő formát: gmdate('D, d M Y H:i:s \G\M\T', $timestamp)
. Ez egy abszolút időpont, ami egyben egyik komoly esetleges hátrányát is jelenti, mert megfelelő működéséhez szükséges, hogy a webszerver és a cache órája szinkronizálva legyen.Last-Modified
fejléc: az erőforrás utolsó módosításának időpontját tartalmazza.ETag
fejléc: ez már a HTTP 1.1-es verziója által bevezetett fejléc. Tartalma mindig egy adott erőforrás egy adott verzióját azonosítja. Például egy fájl esetén az ETag
lehet a fájl tartalmának md5
kódja.Cache-Control
fejléc: ez a nagy dobás a HTTP 1.1-es verziójában, mely lényegesen több lehetőséget biztosít számunkra, mint az eddig bemutatottak. Most csak a válaszra vonatkozó részét nézzük meg, melynek formátuma:Cache-Control = "Cache-Control" ":" 1#cache-response-directive
cache-response-directive =
"public" ; Section 14.9.1
| "private" ; Section 14.9.1
| "no-cache" ; Section 14.9.1
| "no-store" ; Section 14.9.2
| "no-transform" ; Section 14.9.5
| "must-revalidate" ; Section 14.9.4
| "proxy-revalidate" ; Section 14.9.4
| "max-age" "=" delta-seconds ; Section 14.9.3
| "s-maxage" "=" delta-seconds ; Section 14.9.3
cache-response-directive =
"public" ; Section 14.9.1
| "private" ; Section 14.9.1
| "no-cache" ; Section 14.9.1
| "no-store" ; Section 14.9.2
| "no-transform" ; Section 14.9.5
| "must-revalidate" ; Section 14.9.4
| "proxy-revalidate" ; Section 14.9.4
| "max-age" "=" delta-seconds ; Section 14.9.3
| "s-maxage" "=" delta-seconds ; Section 14.9.3
Részei:
public
: a válasz bármilyen cache-ben tárolásra kerülhet.
private
: a válasz csak nem publikus (jelen esetben ez a böngésző) cache-ben kerülhet tárolásra.
no-cache
: ez azt jelenti, hogy a cache köteles minden esetben az ilyen fejléccel érkezett és eltárolt tartalmat a kiszolgáló szerver által hitelesíteni, mielőtt azt kiszolgálná másnak.
no-store
: Ezeket a válaszokat nem szabad semmilyen maradandó tároló egységen megőrizni, sőt elvileg a cache-nek törekednie kell arra is, hogy akár csak temporáris helyen is minél kevesebb ideig tárolja.
must-revalidate
: a cache-nek mindig érvényesítenie kell az oldalhoz érkező kérelmeket.
proxy-revalidate
: mint az előző, de csak a proxy cache-ekre vonatkozik (pl. böngészőre nem).
max-age
: másodpercben mérve az az idő, amíg a válasz gyorstárban tárolható.
s-maxage
: mint az előző, de csak a proxy cache-ekre vonatkozik. Ez utóbbi kettő bármelyikét beállítjuk, akkor a válaszban esetlegesen szereplőExpire
fejlécet felülbírálják.
Amit fontos észrevennünk, hogy itt külön képesek vagyunk mgekülönböztetni a böngésző és a köztes cache-eket. Például egy felhasználó saját tartalmainak cache-elését nem érdemes engedélyeznünk egy köztes cache-ben, csak a saját böngészőjében.
Validátorok, validálás
A előző részben többször elhangzott a validálás fogalma, lássuk is, hogy pontosan miről is van szó. A validátorok nagyon fontosak a cache-elés mechanizmusában: ha egy válasz nem tartalmaz egyetlen validátort sem, és egyéb cache-elhetőségről szóló információt (Expire
, Cache-Control
) sem, akkor semmilyen cache-be sem kerülhet be. Jelenleg két validátor van: Last-Modified
(ez a legszélesebb körben támogatott) és az ETag
.Amikor a cache-nek validálnia kell egy oldalt, akkor elküldi a szervernek a válaszban kapott
Last-Modified
fejléc tartalmát egy If-Modified-Since
fejléc tartalmaként, illetve az ETag
fejlécben kapott információt egy If-None-Match
fejléc tartalmaként. A kérésből a szerver meg tudja állapítani, hogy kért oldal megváltozott-e, ha nem, akkor egy HTTP/1.1 304 Not Modified
választ küld, ha igen, akkor küldi a megváltozott tartalmat.Egy lehetséges megoldás lehet a következő:
<?php
$eTag = md5($file);
$fModTime = gmdate("D, d M Y H:i:s", filemtime($my_file))." GMT";
if($_SERVER["HTTP_IF_NONE_MATCH"] == $eTag ||
$_SERVER["HTTP_IF_MODIFIED_SINCE"] == $fModTime)
{
header("HTTP/1.1 304 Not Modified");
header("Last-Modified: ".$fModTime."");
header("ETag: ".$eTag."");
exit;
}
?>
$_SERVER
tömbben, de vannak amit ezen az úton nem érünk el. A példában használjuk az ETag
validátort is, de jó tudni, hogy az Internet Explorer támogatása ezen a téren (is ;-)) hagy némi kívánni valót, ezért mindenféleképpen használjuk mellett a Last-Modified
-t is. Megkülönböztetünk erős és gyenge validátorokat. Az erős validátor megváltozik, ha a válasz egyetlen bitje is változik. Egy gyenge validátor csak akkor változik, ha a válasz jelentése változik meg. Erős validátorokra ott lehet például olyan cachek esetén van szükség, melyek képesek a válasz egy részletére történő kérés kiszolgálására. Ha ez nem szükséges, akkor egy gyenge validátor használatával lényegesen jobb lehet a cache teljesítménye.
Állományok részleges kiszolgálása
Könnyen előfordulhat, hogy egy kliens kérésének kiszolgálása közben megszakad a kapcsolat. Ennek eredménye, hogy egy befejezetlen válasz (fájl). Ilyenkor a kérés újraküldésekor hasznos lehet, ha jelezhetjük a szerver számára, hogy az igényelt adat egy részével már rendelkezünk, nem szükséges a teljes válasz újraküldése, elegendő csak a hiányzó részeké. Ehhez a következő fejlécekre van szükségünk.Accept-Ranges
: Ezzel a fejléccel jelezhetjük a kliensnek, hogy a szerver képes fogadni olyan kéréseket, ahol a kliens megadhatja, hogy honnan történjen az adott fájl kiszolgálása. A jelenlegi HTTP/1.1 szabvány szerint ennek az értéke "none", vagy "bytes" lehet. A szervernek nem kötelező ilyen fejlécet kiadnia.Content-Range
ha egy teljes állománynak csak egy részét küldjük ki, akkor ezzel a fejléccel jelezzük, hogy ez az egésznek mely részét is képzi. A pontos definiciója:Content-Range = "Content-Range" ":" content-range-spec
content-range-spec = byte-content-range-spec
byte-content-range-spec = bytes-unit SP
byte-range-resp-spec "/"
( instance-length | "*" )
byte-range-resp-spec = (first-byte-pos "-" [last-byte-pos]) | "*"
instance-length = 1*DIGIT
first-byte-pos = 1*DIGIT
last-byte-pos = 1*DIGIT
content-range-spec = byte-content-range-spec
byte-content-range-spec = bytes-unit SP
byte-range-resp-spec "/"
( instance-length | "*" )
byte-range-resp-spec = (first-byte-pos "-" [last-byte-pos]) | "*"
instance-length = 1*DIGIT
first-byte-pos = 1*DIGIT
last-byte-pos = 1*DIGIT
Nem elrettentésnek szánom, csak szokjunk egy kis RFC szintaxist :-). Szépen sorban:
- A fejléc a "Content-Range" literálból, a ":" literálból és a
content-range-spec
szabályból (rule) áll.
- A
content-range-spec
felépítése:bytes-unit
szabály, ezt egy space követi, majd abyte-range-resp-spec
szabály, utána egy "/", majd végül vagy egyinstance-length
vagy egy "*" literál jön. Abytes-unit
értéke jelenleg csak a "bytes" lehet. Abyte-range-resp-spec
határozza meg, hogy mely részről van szó. Ezt követi egy "/" jel, majd a teljes állomány hosszának meghatározása. Ez egy (legalább egy számjegyból álló) szám, vagy a "*" karakter, ami azt jelenti, hogy a küldés pillanatában nem ismerjük a teljes hosszt.
- A
byte-range-resp-spec
felépítése pedig vagy egy kezdő byte pozíció, "-", utolsó byte pozíció (opcionális), vagy pedig a "*" karakter.
A kezdő byte pozíció a 0. Az utolsó byte pozíció (ha megadták) nagyobb kell legyen, mint a kezdő, és kisebb kell legyen, mint a teljes hossz. Ellenkező esetben a kliensnek a választ érvénytelennek kell tekintenie. Ha nem megfelelő a kérésben megadott tartomány, akkor 406-os státusz kódú (Requested range not satisfiable - Kért tartomány nem kielégíthető) választ kell adnunk, ilyenkor használhatjuk a
Content-Range
fejlécben a "*" tartalmú byte-range-resp-spec
-t. Ha a tartomány kiszolgálható, akkor a választ 206-os státusz kóddal (Partial Content - Részleges tartalom) kell kiküldenünk, ilyenkor tilos "*" byte-range-resp-spec
megadása.Például egy 1kb-os fájl első és utolsó 100 byte-jának kiszolgálásakor a fejléc így néz ki:
Content-Range: bytes 0-99/1024
Content-Range: bytes 924-1023/1024
Content-Range: bytes 924-1023/1024
Range
fejléc: ennek megadásával tudja a kliens elkérni a tartalom megfelelő részletét.Range = "Range" ":" ranges-specifier
ranges-specifier = byte-ranges-specifier
byte-ranges-specifier = bytes-unit "=" byte-range-set
byte-range-set = 1#( byte-range-spec | suffix-byte-range-spec )
byte-range-spec = first-byte-pos "-" [last-byte-pos]
first-byte-pos = 1*DIGIT
last-byte-pos = 1*DIGIT
suffix-byte-range-spec = "-" suffix-length
suffix-length = 1*DIGIT
ranges-specifier = byte-ranges-specifier
byte-ranges-specifier = bytes-unit "=" byte-range-set
byte-range-set = 1#( byte-range-spec | suffix-byte-range-spec )
byte-range-spec = first-byte-pos "-" [last-byte-pos]
first-byte-pos = 1*DIGIT
last-byte-pos = 1*DIGIT
suffix-byte-range-spec = "-" suffix-length
suffix-length = 1*DIGIT
Ezt már értjük is ;-), gyors magyarázat: megadhatunk egy tartományt (
byte-range-spec
), vagy pedig egy állomány utolsó x. byte-ját kérhetjük el. Előbbi esetben a végpozició mgeadása nem kötelező, ilyenkor az állomány méretéből adódik. Itt is a Content-Range
fejlécnél már megismert módon kell eljárni, ha a kérés szintaktikailag nem megfelelő, akkor a válasz 406, ellenkező esetben kérés kiszolgálása 206-os státusz kóddal. Pár gyors példa (a szobanforgó fájl hossza 10000 byte):- első 500 bájt:
bytes=0-499
- második 500 bájt: bytes=500-999[/code]
- utolsó 500 bájt:
bytes=-500
vagybytes=9500-
- utolsó és első bájt:
bytes=0-0,-1
És végezetül lássunk egy nem teljesne kidolgozott kódot arra, hogy hogyan is tudunk egy ilyen kérést kiszolgálni. És ezt már magunktól később be tudjuk építeni saját, vagy a fentebb látott letoltő scriptbe. Elöljáróban, mint említettem bizonyos fejléceket nem tudunk a
$_SERVER
tömbbön keresztül elérni, csak a getallheaders
függvény segítségével.<?php
$fileSize = filesize('letoltendo.allomany');
$headers = getAllHeaders();
if(isset($headers["Range"])) {
// Ez a függvény a különböző lehetséges megadási módok
// figyelembe vételével.
$params = getRangeParam($headers["Range"], $fileSize);
if ($params !== false) {
header("HTTP/1.1 206 Partial content");
header("Content-Length: ".$params['length']);
header("Content-Range: bytes $params['from']."-".$params['to']."/".$fileSize;");
header("Connection: close");
// Fájl megfelelő részének kimenetre írása
} else {
header("HTTP/1.1 406 Requested range not satisfiable");
}
}
?>
No hát ennyi lenne a mai adag, majd egyszer talán leírom, hogy hogy született meg ez a cikk, nem volt egyszerű, egész regényesre sikeredett (1-2 hónapnyi Barátok közt ízgalmával simán vetekedne...
Ismét egy nagyon jó cikket
Csak egy dolgot nem értek: Miért van kint az egész anyag a főlapon? nem úgy szokott lenni, hogy csak a bevezető került ki? vagy ez is az újításokkal jött?
Miért van kint...
De majd biztos kijavítják a fijúk...
Gyulus
Javítva
Nagyszerű cikk!
max_execution_time?
semmi jó
szerencsére nem igaz
forrás: http://hu.php.net/function.set-time-limit
azaz csak a tényleges script-futás számít bele a limitbe, egyéb dolgok (pl. system, adatbázis-kezelés, stream-kezelés) NEM!
vagy mégis?
és valóban
fpassthru()
egyfopen()
vagyfsockopen()
által adott erőforrás azonosítót vár, amik ugye stream azonosítót adnak vissza, tehát streamről beszélünk...Ez egy picit árnyaltabb
speed limit
Kérdés
A cikk nagyon tanulságos. Egy kérdésem lenne hozzá, remélem tud valaki segíteni.
Tehát: Miért nem veszi figyelembe az IE a Disposition-ban megadott filenet? Mindig a download.php a default, mert ez a scriptfile neve. Mit rontottam el? webServer: IIS 5.0 vagy Apache, mind a 2 esetben ezt csinálja. :(
Segítsétek, legyetek szivesek. Előre is köszönöm.
BR
Kód?
Ilyen esetekben célszerű lehet egy letisztított példakódot is mutatni, hátha az alapján jobb választ tudunk adni. Esetleg egy kipróbálható verzió még jobb lenne.
Én amúgy nem tapasztaltam ilyesmit.
Felhő
Szia! Ha egy az egyben az
Ha egy az egyben az "alap megoldás" scriptet használtam, akkor is a download volt a default filenév. :( Ötlet?
Előre is köszi.
Bodnár Róbert
Folytatható?
A fentiek segítségével nem sikerült folytatható letöltést létrehoznom, mert mindig leáll azzal, hogy a fájlméret megváltozott.
Mi ebben a hiba?
<?php
$disposition = "attachment";
$mimeType = "application/octet-stream";
$fileName = "xxx.xxx";
$path = "/var/www/".$fileName;
$fileSize = filesize($path);
$headers = getAllHeaders();
if(isset($headers["Range"])) {
$params = getRangeParam($headers["Range"], $fileSize);
if ($params !== false) {
header("HTTP/1.1 206 Partial content");
header("Content-Type: ".$mimeType);
header("Content-Disposition:".$disposition."; filename=\"".trim(htmlentities($fileName))."\"");
header("Content-Description: ".trim(htmlentities($fileName)));
header("Content-Length: ".$fileSize);
header("Content-Range: bytes ".$params['from']."-".$params['to']."/".$fileSize.";");
header("Connection: close");
$file = fopen($path,"rb");
fseek ($file, $params['from']);
$bufferSize = 1024;
while(!feof($file)) {
echo fread($file, $bufferSize);
}
fclose($file);
} else {
header("HTTP/1.1 406 Requested range not satisfiable");
}
}
else {
header("Content-Type: $mimeType");
header("Content-Disposition:".$disposition."; filename=\"".trim(htmlentities($fileName))."\"");
header("Content-Description: ".trim(htmlentities($fileName)));
header("Content-Length: ".$fileSize);
header("Cache-Control: must-revalidate, post-check=0, pre-check=0");
header("Connection: close");
$bufferSize = 1024;
$file = fopen($path,"rb");
while(!feof($file)) {
echo fread($file, $bufferSize);
}
fclose($file);
}
?>
Sziasztok!
A probléma a következő: Hol van a getRangeParam függvény? Alap php függvényként nem találtam ilyet..
Sebaj, összedobtam egyet, ami nagyon nem tökéletes, de az alap esetekhez megteszi....
Íme:
$disposition = "attachment";
$mimeType = "application/octet-stream";
$fileName = "face_02.jpg";
$path = "fajlok/".$fileName;
$fileSize = filesize($path);
$headers = getAllHeaders();
if(isset($headers["Range"])) {
//Hiányzó függvény pótlása
//elhagyjuk az első 6 karaktert, helyesebben a 6+1.-től vesszük (mivel az első száma 0!)
$range = substr("$headers[Range]", "6");
//A kötőjeleknél felbontjuk.
$range_darab = explode("-", "$range");
//Ebben az esetben megvan adva pontosan, hogy melyik byte-tól melyikig kell.
if ($range_darab["0"] != NULL AND $range_darab["1"] != NULL) {
$params['from'] = $range_darab["0"];
$params['to'] = $range_darab["1"];
$params_if = true;
//Itt csak a kezdő byte van megadva, végső byte nincs, azaz a fájl végéig kell minden
//Így adja meg a FLASHGET is.
} else if ($range_darab["0"] != NULL AND $range_darab["1"] == NULL) {
$params['from'] = $range_darab["0"];
$params['to'] = $fajlmeret - 1; // -1: mivel 0 az első byte, a fájlméret meg ugye 1 byte-nál kezdődik
$params_if = true;
//Itt a fájl végétől számított, hátralévő byte-ok vannak megadva db-ra.
} else if ($range_darab["0"] == NULL AND $range_darab["1"] != NULL) {
$params['from'] = $fajlmeret - 1 - $range_darab["0"];
$params['to'] = $fajlmeret - 1;
$params_if = true;
} else {
$params_if = false;
}
//NEM TÖKÉLETES: http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.35.2 a cím fölötti utolsó két eset nincs megoldva!
// getRangeParam($headers[Range], $fileSize);
if ($params_if == true) {
header("HTTP/1.1 206 Partial content");
header("Content-Type: ".$mimeType);
header("Content-Disposition:".$disposition."; filename=\"".trim(htmlentities($fileName))."\"");
header("Content-Description: ".trim(htmlentities($fileName)));
header("Content-Length: ".$fileSize);
header("Content-Range: bytes ".$params[from]."-".$params[to]."/".$fileSize.";");
header("Connection: close");
$file = fopen($path,"rb");
fseek($file, $params['from']);
$bufferSize = 1024;
while(!feof($file)) {
echo fread($file, $bufferSize);
}
fclose($file);
} else {
header("HTTP/1.1 406 Requested range not satisfiable");
}
} else {
header("Content-Type: $mimeType");
header("Content-Disposition:".$disposition."; filename=\"".trim(htmlentities($fileName))."\"");
header("Content-Description: ".trim(htmlentities($fileName)));
header("Content-Length: ".$fileSize);
header("Cache-Control: must-revalidate, post-check=0, pre-check=0");
header("Connection: close");
$bufferSize = 1024;
$file = fopen($path,"rb");
while(!feof($file)) {
echo fread($file, $bufferSize);
}
fclose($file);
}
?>
Remélem segítettem!
Content-Length
Letöltés wapról, telóval
Nagyon jó ez a cikk !
Abban kérném segítségeteket, hogy hogyan tudok egy ilyen letöltést úgy megcsinálni, hogy telefonnal is lehessen letölteni ?
Előr eis THX!
Letöltés wapról, telóval
Érdemes a letöltés sebességének állítgatásakor figyelembe venni a GPRS kapcsolatok sávszélességét, mivel egyelőre még sok ilyen letöltőre lehet számítani-
IE off
Ékezetes nevek a fájlnévben
A megoldás a következő: =?karakterkészlet?kódolás?szöveg?= "inline" karakterkészlet-váltás.
A kódolás lehet Q vagy B (mint quoted printable és base64, mert még mindig nem írhatunk mást, mint az ASCII karaktereket)
A karakterkészlet értelemszerűen ISO-8859-2 vagy inkább UTF-8
Példa:
Én az alábbi kis szösszenetet gyártottam magamnak:
Internet Explorer 7 base64 attachment filename
Csak nálam nem működik?
Letöltésre küldöm és az elkodolt nevet adja mentéskor nem az valóst.
Persze Firefoxban OK.
rand
egy elgépelés:
Mellesleg jó ez az összefoglalás.. teccik :)
"regényes" kezdet...?
És ez le lett írva valahol? :)