یک وبلاگ دیگر از یک برنامه نویس دیگر
دانلود فایل با قابلیت Resume در PHP
کد انتهایی یه مشکل کوچک داشت که اصلاح شد :)
ایندفعه،میخوام یه چیزی بنویسم درباره دانلود (آخرش هم نفهمیدم که این کلمه رو به فارسی چی بگیم!! بارگزاری،بارگیری، گرفتن ..) خیلی وقتها میخوایم که یه فایل رو محافظت کنیم که همینجوری دانلود نشه، یعنی حتما اسم کاربری بخواد، یا اینکه مطمئن شیم که فایل حتما از تو صفحه خودمون دانلود میشه که آمار درست باشه.
یه راه ساده وجود داره، که بیشتر از اون استفاده میشه،اونهم به سادگی گذاشتن فایل در یک پوشه خارج از دسترسی مستقیم (مثلا خارج از ریشه وب سرور، یا محافظت شده توسط وب سرور،مثلا آپاچی با کمک فایل .htaccess ) و بعد به پس دادن فایل از طریق کد به کاربر. مثلا برای PHP میتونید همچین کدی بنویسید :
//Before this point you should check everything
//include user authenticate and any thing else
$result=@readfile('/path/to/file');
if ($result===false)
//Error :(
else {
// $result contain byte count
}
این روش بد نیست، کاربر نمیتونه لینک مستقیم بگیره، که خوب این معمولا برای خیلی ها مهمه، خصوصا با این وضع وبلاگها و سایتهای ایرانی که متخصص کپی/پیست و گرفتن لینک و گذاشتن به اسم خودشون حتی بدون زحمت آپلود مجدد هستن.
اما یه عیب بزرگ داره،این دانلود دیگه قابلیت resume نداره. منم که دقیقا میتونم کاربر دیال آپی رو درک کنم،مخصوصا اینکه تو ایران بزرگ شدم!! خوب این شد که نشستم و بررسی کردم که چطوری این مشکل رو هم حل کنم. البته این کارو خیلی وقت پیش انجام دادم، و چون امروز خواهر زاده ام گیر داده بود، پیداش کردم و یادم افتاد که میشه اینجا بنویسمش،و خصوصا اینکه این روزا همش تو فکر اینم که چی بنویسم که ارزش نوشتن داشته باشه .
اول باید یه توضیح ساده بدم. اونم اینکه اصلا این روش resume چطور کار میکنه. واسه اینکار سرور باید یه اطلاعاتی رو به صورت header بفرسته به کلاینت در ازای درخواست کلاینت. این کار رو به راحتی میشه انجام داد :
header('Accept-Ranges: bytes');
این رو وب سرورها برای هر فایلی که قابلیت resume بخواد داشته باشه میفرستن. البته اگه خود سرور این قابلیت رو داشته باشه. خوب ما هم همینکار رو انجام میدیم یعنی این خط رو به جواب کلاینت اضافه میکنیم، اینجوری حتی اگه خود وب سرور هم این قابلیت رو نداشته باشه ما این قابلیت رو اضافه کردیم (یه بار یه وب سرور مینوشتم، با دلفی و ایندی و خودم این قابلیت رو اونجا اضافه کردم، خیلی سخت نبود :) )
حالا کلاینت وقتی میفهمه که وب سرور این قابلیت رو داره ، علاوه بر آدرس فایل یه سری اطلاعات هم میفرسته.ما کاری با کلاینت نداریم،چون تو این حالت کلاینت برنامه دانلود هست، که خوب از بحث ما جداست. این اطلاعات رو میتونید (توی PHP ) از آرایه $_SERVER بخونید. این اطلاعات عبارتند از :
$ranges= $_SERVER['HTTP_RANGE']; //Now ranges contain some thing like : Range: bytes=0-500
البته توی Draft مربوط به این قضیه که من خوندم، که Range میتونه چند تایی باشه که من کاری به اون قضیه ندارم و اصولا هم تا به حال ندیدم تو عمل این چند تایی بودن رو. اما این دو عدد که با یه منها از هم جدا میشن، نشونگر بایتهایی هستند که خواسته شده. علامت منها همیشه باید باشه. اما یکی از دو عدد میتونه نباشه (دو عدد همزمان نمیتونن نباشن) اگه عدد اول نباشه، یعنی یه منها باشه بعد عدد دوم، به معنی درخواست n بایت انتهایی فایل هست، که n میشه همون عدد دوم.
اما اگه عدد دوم نباشه، یعنی یه عدد m بعد یه منها،به این معنیه که از بایت m شروع کن تا آخر فایل خواسته شده. یادتون هم باشه که بایتها از صفر شروع میشن، یعنی اولین بایت صفره.
خوب این درخواست که بیاد شما باید پاسخ بدید. پاسخ هم ساده هستش:
header("HTTP/1.0 206 Partial Content");
header("Status: 206 Partial Content");
header('Accept-Ranges: bytes');
دو تای اولی میگن که اطلاعاتی که قراره با این اتصال فرستاده بشن، یه تیکه از فایل هستن نه یه فایل کامل. خط سوم هم که بالاتر گفتم. البته یادتون باشه header های دیگه مثلا اینها هم باید باشن :
header('Content-type: ' . $mime);
header('Content-Disposition: attachment; filename="' . $filename . '"');
header('Last-Modified: ' . date('D, d M Y H:i:s \G\M\T' , $data_modifed));
یا خیلی header های استاندارد دیگه، تو این مثال اولی نوع فایل رو مشخص میکنه مثلا image/jpeg یا application/otect-stream یا … دومی هم اسم واقعی فایل رو میگه خصوصا اینکه ما داریم فایل رو طوری میفرستیم که آدرسش معلوم نشه، ولی بهتره اسم فایل رو معلوم کنیم که نرم افزار کلاینت یعنی همون دانلود منیجر یا بروزر بفهمه که اسم فایل چیه تا از اسم فایل php مثلا download.php استفاده نکنه. سومی هم که زمان آخرین دستکاری فایله، و خوب جز اینها باز هم میتونه باشه یه چک بکنید میتونید همه رو تو یه جستجوی ساده توی وب پیدا کنید.
اما حالا باید به کلاینت بگیم که چه بایتهایی رو داریم میفرستیم و چند بایت داریم میفرستیم ، به سه متغییر استفاده شده دقت کنید تو کامنتهای بالای کد نوشتم که هر کدوم چی هست :
//$size : size of file or data (all data not this part)
//$seek_start : start of data in file, for example in ( Range: bytes=0-500 ) $seek_start=0
//$seek_end : end of data in file, for example in ( Range: bytes=0-500 ) $seek_end=500
header("Content-Range: bytes $seek_start-$seek_end/$size");
header("Content-Length: " . ($seek_end - $seek_start + 1));
خوب،دیگه header کافیه وقت اطلاعات واقعی هستن که فرستاده بشن. این اطلاعات میتونه هر چی باشه، از دیتابیس باشه، از فایل واقعی باشه یا… من فرض رو بر فایل واقعی میذارم. خوب ما گفتیم که یه قسمت از فایل رو قراره که بفرستیم نه همه اونو،پس وقتشه که شروع کنیم، فایل رو باز کنیم، اون قسمت مورد نظر رو بخونیم، و بعد مستقیم توی خروجی بنویسیم، مثلا با یه echo ساده. البته مشکلی به وجود میاد اونم برای فایلهای بزرگ و رنجهای بزرگ. یعنی مثلا یه فایل ۱ گیگا داری، برنامه دانلود میزنه ۴ قسمت ۲۵۰ مگابایتی درخواست میکنه. خوب اینجا مشکله که کل فایلو یه جا بخونی و بریزی بیرون، چون معمولا PHP برای استفاده از حافظه محدودیت داره. برای رفع این مشکل یه راه هست و اونم اینه که فایل رو تکه تکه بخونی مثل این حالت :
$data_len=$seek_end-$seek_start;
fseek($file,$seek_start,SEEK_SET);
$bufsize=2048;
ignore_user_abort(true);
@set_time_limit(0);
while (!(connection_aborted() || connection_status() == 1) && $data_len > 0){
if ($data_len < bufsize)
echo fread($file , $data_len);
else
echo fread($file , bufsize);
$data_len -= $bufsize;
flush();
}
اون سه تا تابع ignore_user_abort و connection_aborted و connection_status به ما کمک میکنن که کنترل پایان عمل رو از کاربر بگیریم که برای این مورد اینکار بهترین کاره(یعنی اگه کاربر عمل دانلود رو لغو کنه بلافاصله اسکریپت متوقف نمیشه، ادامه پیدا میکنه تا درست و حسابی متوقف بشه. ). از طرفی با set_time_limit محدودیت زمان اجرای PHP رو هم از بین میبریم که تو این مورد خیلی مهمه، چون PHP بعد از ۳۰ ثانیه به صورت اتوماتیک متوقف میشه و اگه دانلود زیاد طول بکشه این زیاد جالب نیست. اندازه بافر رو هم ۲۰۴۸ بایت در نظر گرفتم که میشه تغییرش داد.
کد کامل هم میشه این (که البته شما میتونید اونو به صورت یه کلاس دربیارید، من بیشتر قصدم این بود که توضیح بدم همه چیزو نه اینکه یه کلاس کامل بنویسم)
<?php
date_default_timezone_set('GMT');
//1- file we want to serve :
$data_file="/usr/home/f0rud/Desktop/largefile";
$data_size=filesize($data_file);
$mime='application/otect-stream'; //Mime type of file. to begin download its better to use this.
$filename=basename($data_file); //Name of file, no path included
//2- Check for request, is the client support this method?
if (isset($_SERVER['HTTP_RANGE']) || isset($HTTP_SERVER_VARS['HTTP_RANGE'])){
$ranges_str=(isset($_SERVER['HTTP_RANGE']))?$_SERVER['HTTP_RANGE']:$HTTP_SERVER_VARS['HTTP_RANGE'];
$ranges_arr=explode('-', substr($ranges_str , strlen('bytes=')));
//Now its time to check the ranges
$ranges_arr[0]=intval($ranges_arr[0]);
if ((intval($ranges_arr[0])>=intval($ranges_arr[1]) &&
$ranges_arr[1]!="" &&
$ranges_arr[0]!="" ) ||
($ranges_arr[1]=="" && $ranges_arr[0]=="")){
//Just serve the file normally request is not valid :(
$ranges_arr[0]=0;
$ranges_arr[1]=$data_size;
}
} else { //The client dose not request HTTP_RANGE so just use the entire file
$ranges_arr[0]=0;
$ranges_arr[1]=$data_size;
}
//Now its time to serve file
$file=fopen($data_file,'rb');
//I use seek and tell to find the location, since I'm too lazy now
//You may use some + or - instead of all this :)
if ($ranges_arr[0]==""){
//Status 1 : the first one dose not exist
fseek($file, - intval($ranges_arr[1]),SEEK_END);
$seek_start=ftell($file);
fseek($file, intval($ranges_arr[1]),SEEK_CUR);
$seek_end=ftell($file);
}elseif ($ranges_arr[1]==""){
//Status 2 : the last one dose not exist
fseek($file,intval($ranges_arr[0]),SEEK_SET);
$seek_start=ftell($file);
fseek($file, $data_size - intval($ranges_arr[1]),SEEK_CUR);
$seek_end=ftell($file);
}else{
//Status 3 : Both are here :)
fseek($file,intval($ranges_arr[0]),SEEK_SET);
$seek_start=ftell($file);
fseek($file, intval($ranges_arr[1])-intval($ranges_arr[0]),SEEK_CUR);
$seek_end=ftell($file);
}
//Lets send headers
header('HTTP/1.0 206 Partial Content');
header('Status: 206 Partial Content');
header('Accept-Ranges: bytes');
header('Content-type: ' . $mime);
header('Content-Disposition: attachment; filename="' . $filename . '"');
header("Content-Range: bytes $seek_start-$seek_end/$data_size");
header("Content-Length: " . ($seek_end - $seek_start));
//Finally serve data and done ~!
$data_len=$seek_end - $seek_start;
fseek($file,$seek_start,SEEK_SET);
$bufsize=2048;
ignore_user_abort(true);
@set_time_limit(0);
while (!(connection_aborted() || connection_status() == 1) && $data_len > 0){
if ($data_len < $bufsize)
echo fread($file , $data_len);
else
echo fread($file , $bufsize);
$data_len -= $bufsize;
flush();
}
fclose($file);
?>
من اینو با FDM و DownThemAll تست کردم. اگه کسی با نرم افزار دیگه تست کرد و جواب داد همینجا بگه. یه چیز عجیب اینه که WGET با این کار نمیکنه مدام در مورد Partial Content خطا میده :) به هر حال .
اصلاحیه برای IE8
دوستی توی این کامنت نوشتن که این کد با اینترنت اکسپلورر ۸ مشکل داره و علتش هم این باگ هستش : Cannot Download .pdf File with HTTP 1.1 Cache-Control = “no-cache” Directive
ایشون به عنوان راه حل گفتن که بایستی این دو خط کد هم به Header پاسخ اضافه بشه به عنوان رفع مشکل :
header("Cache-Control: no-cache");
header("Pragma: no-cache");
این کد باید بین خطوط ۵۸ تا ۶۰ قرار بگیره. فعلا هنوز امکان امتحانشو ندارم. بعد از بررسی دقیقتر حتما اصلاحش رو خواهم نوشت. فعلا اصلا در شرایط نوشتن کد نیستم. بازم ممنون از این دوست.
| چاپ این نوشته | این نوشته توسط فرود در روز 2010/04/10 در ساعت 00:52 نوشته شده است و در دسته PHP, برنامه نویسی دسته بندی شده است. پاسخهای این نوشته را از طریق RSS 2.0 دنبال کنید. شما میتوانید نظر خود را ارسال کنید یا از سایت خود دنبالک ارسال کنید. |





در 1 سال پیش
اول این مقاله جالب بود. خیلی به درد من می خوره.
دوم این کلید کنترل بعلاوه G خیلی باحاله! خودتون نوشتید؟ یا از افزونه های وردپرس هست؟
سوم چرا داخل درباره، یک آدرس ایمیل نگذاشتید؟ اومد و یکی مثل من خواست پیشنهاد کاری بده به شما!
موافقید یا نه :
0
0
در 1 سال پیش
خوشحالم که به درد کسی خورد :)
نه این از Google Translator هستش.
آدرس ایمیل :) قصدشو داشتم اما نمیدونم چرا اینکار رو نکردم. به همین زودی :)
موافقید یا نه :
0
0
در 1 سال پیش
آفرین – جالب بود
موافقید یا نه :
0
0
در 1 سال پیش
سلام آقا فرود
خیلی آموزش خوبیه
میخوام از کدت تو سایتم استفاده کنم انشاالله که مشکلی پیش نمیاد
به سایتم یه سر بزن و نظرت رو درباره سایتم بهم بگو اگه تونستی باگاشم بهم بگو
اگه هم خواستس ثبت نام کن
موافقید یا نه :
0
0
در 1 سال پیش
کار نمیکنه
فایل رو خراب دانلود میکنه
موافقید یا نه :
0
0
در 1 سال پیش
والا من که با چند تا دانلودر تست کردم و بدون مشکل داره کار میکنه. با چی دانلود کردید شما؟؟من FDM ، IDM ، DAP رو تست کردم.
برای خود فایرفاکس یا اینترنت اکسپلورر بدون برنامه هم درست کار میکنه فقط با wget مشکل داشت که بعید میدونم شما با اون تست کرده باشید.
موافقید یا نه :
0
0
در 1 سال پیش
آقا سلام. مطلب خیلی کاربردی بود. فقط یه فکری واسه ظاهر سایتت بکن. تو opera بهم میریزه. ناجوز هم بهم میریزه. قسمت کدها مخصوصاً
تازه آشنا شدم با سایتت.
یا حق
موافقید یا نه :
0
0
در 1 سال پیش
والا من دارم چک میکنم، ولی مطلقا هیچ مشکلی با اپرا ندارم!
هم رو لینوکس و هم رو ویندوز.
موافقید یا نه :
0
0
در 1 سال پیش
آقا دوباره سلام.
مشکل داره، یعنی من هنوز مشکل دارم تو اپرا!
کد آخرت تو فایر فاکس به چپ و راست اسکرول داره، اما تو اپرا این اسکرول نیس! یعنی ابتدای کدات نیس.
لوگوی سایت هم جدا از همه کلماتش!
فک کنم کاره letter-spacing:-0.04em; باشه که گذاشتی!
باز شما اوستاتری
در ضمن این کد رو من استفاده کردم تو سایتم. عالی بود. یه مشکل اصلی رو واسه پنهان کردن مسیر اصلی فایل، برطرف کرد.
موافقید یا نه :
0
0
در 1 سال پیش
بله :) ممنون واسه گفتنش و البته این مشکل افزونه ای هست که برای کدها استفاده کردم. در اولین فرصت سعی میکنم اصلاحش کنم. در مورد لوگو هم چون با یه فونت فارسی اینکار رو انجام دادم این مشکل پیش اومده. اگه مشکلاتی که فعلا دارم برطرف بشن میخوام یه کم با cufon سر و کله بزنم و کدی رو که برای پورتش به فارسی نوشتم تکمیل کنم، اونوقت خیلی از مشکلات حل میشه.
اما درباره اون دو تا هدر که گفتید، من پست رو ویرایش میکنم تا زمانی که بتونم خودم چکش کنم و دقیقتر بنویسم. بازم ممنون.
موافقید یا نه :
0
0
در 1 سال پیش
آقا این کد تو IE8 کار نمی کنه.
یه همچی اروری میده:
Microsoft Internet Explorer
Internet Explorer cannot download from the Internet site File_name from Computer_name.
The download file is not available. This could be due to your Security or Language settings or because the server was unable to retrieve the requested file.
اینم صفحه اون تو مایکروسافت کوفتی!
http://support.microsoft.com/kb/231296
دلیلشم انگار استفاده نکردن از این دو تا تگه:
header(“Cache-Control: no-cache”);
header(“Pragma: no-cache”);
یعنی اگه از این دو تا تگ استفاده بکنی درست میشه. لطفا اگه تأیید میکنی مقاله رو اصلاح کن.
یا حق
موافقید یا نه :
0
0