ugrás a tartalomhoz

Állományok kiszolgálása PHP-ból

Hodicska Gergely · 2005. Már. 12. (Szo), 12.22
Á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 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);
?>
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 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...
?>
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.

fpassthru

A PHP kézikönyv egyik felhasználói kommentje arra hívja fel figyelmünket, hogy a readfile 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);
?>
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.

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

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;
	}
?>
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 $_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

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 a byte-range-resp-spec szabály, utána egy "/", majd végül vagy egy instance-length vagy egy "*" literál jön. A bytes-unit értéke jelenleg csak a "bytes" lehet. A byte-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

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

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 vagy bytes=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");
        }
    }
?>
Ez 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...
 
1

Ismét egy nagyon jó cikket

tiku I tikaszvince · 2005. Már. 12. (Szo), 14.13
Ismét egy nagyon jó cikket kaptam tőletek!
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?
2

Miért van kint...

Anonymous · 2005. Már. 12. (Szo), 15.01
Gyanítom, hogy a cikk bevezetőja után látható <!--break--> nem működik normálisan.
De majd biztos kijavítják a fijúk...

Gyulus
4

Javítva

Hojtsy Gábor · 2005. Már. 13. (V), 12.50
Volt (nem is egy) hiba a Drupalban, ami ahhoz vezetett, hogy nem működött jól a bevezető generáló... Mivel ezt észleltük és javítottuk, a Drupalban is jobb lesz a dolog a későbbiekben.
3

Nagyszerű cikk!

csk0 · 2005. Már. 13. (V), 00.55
Nagyszerű cikk!
5

max_execution_time?

Anonymous · 2005. Már. 17. (Cs), 07.00
Olyankor mi történik ezekkel a remek php passthru szkriptekkel ha tovább tart a az adott file letöltése, mint 30mp? (php.ini -> max_execution_time default = 30).
6

semmi jó

Hojtsy Gábor · 2005. Már. 17. (Cs), 21.07
Nyilván akkor onnantól nem tudja folytatni a működését egy ilyen szkript. Sajnos. Tehát át kell állítanod a maximális futtatási időt többre akár szkriptből, akár másképp.
8

szerencsére nem igaz

domdom · 2005. Már. 17. (Cs), 23.37
skacok, érdemes az angol manualt is böngészni, nem csak a magyart, mert akkor pl. ilyenekre bukkanhattok:
Note:  The set_time_limit() function and the configuration directive max_execution_time only affect the execution time of the script itself. Any time spent on activity that happens outside the execution of the script such as system calls using system(), stream operations, database queries, etc. is not included when determining the maximum time that the script has been running.

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!
9

vagy mégis?

domdom · 2005. Már. 17. (Cs), 23.43
persze azt nem tudom, hogy az fpassthru stream operationnek (vagy más külső futásnak) számít-e...
11

és valóban

Hojtsy Gábor · 2005. Már. 18. (P), 20.18
Én pedig gyakorlatilag csak az angol kézikönyvet szoktam böngészni :)) Azzal tisztában voltam, hogy az adatbázis műveletek nem számítanak bele, mert erről már volt személyes tapasztalatom, de stream kezelést ilyen volumenben még nem kellett végrehajtanom. Az fpassthru() egy fopen() vagy fsockopen() á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...
7

Ez egy picit árnyaltabb

csla · 2005. Már. 17. (Cs), 21.43
Ez egy picit árnyaltabb dolog, mert a letöltés időtartama nem feltétlenül egyezik meg a kimenet előállításának időtartamával, és azért itt nagyságrendnyi különbségek lehetnek, tehát - persze függ az adott tartalom bonyolultságától - pl. mondjuk 30 sec alatt elég nagy mennyiségű kimenetet elő lehet állítani, aminek aztán a letöltése jóval tovább tarthat. (Nekem pl. egy 700kb-os kimenet előállítása belefér a 3 sec-nyi max_execution_time-ba, de mire a szerverről kliensre letöltöm, az 7 sec.)
10

speed limit

PAStheLoD · 2005. Már. 18. (P), 14.15
Ha szabályozni akarjuk a letöltés sebességét, akkor ez gondolom már máshogy van, tehát akkor a set_time_limit() -al kell ügyeskedni, már amelyik szerver engedni (sajátnál nincs ilyen gond :)) ill ha sleep() -et használunk egy kis várakoztatásra sebességcsökkentéshez, akkor linuxon nem növeljük az exec. time-ot..
12

Kérdés

Anonymous · 2005. Már. 20. (V), 09.46
Sziasztok.
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
14

Kód?

Hodicska Gergely · 2005. Ápr. 3. (V), 07.56
Szia!

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ő
15

Szia! Ha egy az egyben az

Anonymous · 2005. Ápr. 4. (H), 22.26
Szia!
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
13

Folytatható?

KergeKacsa · 2005. Ápr. 1. (P), 20.12
Sziasztok!

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);
}
?>
16

Sziasztok!

Bitman · 2005. Júl. 3. (V), 23.07
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:
<?php

$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!
17

Content-Length

Anonymous · 2005. Aug. 16. (K), 20.52
A gond a kovetkezo: a Content-Length-ben nem a fajl meretet kell megadni, hanem az eppen kiszolgalt darab meretet, tegat a $params['to']-$params['from']-ot.
18

Letöltés wapról, telóval

Anonymous · 2005. Nov. 4. (P), 13.42
Sziasztok !

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!
19

Letöltés wapról, telóval

Anonymous · 2006. Jan. 13. (P), 11.51
Nos, ez szerintem a telefon böngészőjének HTTP támogatásán is múlik. Sajnos mivel minden gyártó más böngészőt használ, a kérdés nem válaszolható meg egyértelműen. Ha ilyet csinálsz, szvsz érdemes beszerezni pár tesztelőt mondjuk Siemens/Nokia/Samung/Sony-Ericsson mobilokkal, mert ezek a leg elterjedtebbek. Amit be lehet tippelni, hogy csak az újabb modelleknél van esély a HTTP 1.1 206-os fejlécének supportjára, ergo át kell futni a browsertől kapott infókat is ilyenkor. Persze ha eleve ilyen kérés érkezik akkor minden további nélkül ki kell szolgálni, hiszen ez azt jelzi,hogy a browser (teló vagy sem) Supportálja a dolgot.

É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-
20

IE off

Anonymous · 2006. Már. 1. (Sze), 19.19
az istennek nem megy az alap scripttel sem IE alatt a normális letöltés. nem ismer fel egy sima .doc fájlt sem, arra is azt mondja, hogy ismeretlen formátum. FF alatt minden oké. + nem lehet kiküszöbölni, hogy elcsesződjenek a fájlnévben lévő ékezetes nevek?
21

Ékezetes nevek a fájlnévben

Anonymous · 2006. Aug. 20. (V), 08.45
A probléma ugye az, hogy a header kódolását nem lehet sehol sem megadni (a dolgot cseppet sem oldja meg a htmlentities, mivel a headerben szó sincs html-ről). Ez a probléma a MIME esetén is előjöhet, így pl. nem lehetne ékezetet írni a tárgy mezőbe.

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:

Content-Disposition: attachment filename="=?UTF-8?B?w4FydsOtenTFsXLFkSB0w7xrw7ZyZsO6csOzZ8OpcA==?=.jpg"
(a .jpg részt csak azért nem kódoltam le, hogy illusztráljam: a string részeiben is kódolható, nem kell az egészet lekódolni)

Én az alábbi kis szösszenetet gyártottam magamnak:

    function utfheader($str)    // a stringek a programban mindenütt UTF-8
                                // kódolásúak
    {
        return '=?UTF-8?B?'.base64_encode($str).'?=';
    }
23

Internet Explorer 7 base64 attachment filename

randomly · 2007. Nov. 11. (V), 12.03
Sziasztok!

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
22

egy elgépelés:

EL Tebe · 2006. Dec. 14. (Cs), 13.01
Egy gépelési hiba a cikkben:
"Amit fontos észrevennünk, hogy itt külön képesek vagyunk mgekülönböztetni"


Mellesleg jó ez az összefoglalás.. teccik :)
24

"regényes" kezdet...?

nevergone · 2008. Május. 29. (Cs), 20.50
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...


És ez le lett írva valahol? :)