PHP: Обнаружение сбоев fopen() при загрузке изображений

У меня есть карточная игра (скриншот ниже), в которой я отображаю аватары игроков.

Для аватаров я написал короткий скрипт proxy.php, который принимает URL-адрес изображения, переданный ему в качестве параметра ?img=, загружает его и сохраняет в папке /var/www/cached_avatars. /md5_of_that_url на моем компьютере с CentOS 5. В следующий раз, когда скрипт будет вызван с тем же URL-адресом, он найдет это изображение в каталоге и передаст его непосредственно в STDOUT.

Это работает в основном хорошо, но для некоторых аватаров первоначальная загрузка не удалась (я полагаю, время истекло), и вы не видите нижнюю часть изображения игрока:

альтернативный текст

Я хотел бы обнаружить сбой загрузки этого изображения и удалить кешированный частичный файл, чтобы он был повторно загружен при следующем вызове proxy.php.

Я пытался обнаружить события STREAM_NOTIFY_FAILURE или STREAM_NOTIFY_COMPLETED в обратном вызове, но они не запускаются. Я вижу только следующие события: STREAM_NOTIFY_CONNECT, STREAM_NOTIFY_MIME_TYPE_IS, STREAM_NOTIFY_FILE_SIZE_IS, STREAM_NOTIFY_REDIRECTED, STREAM_NOTIFY_PROGRESS:

Nov  3 18:48:27 httpd: 2  0
Nov  3 18:48:27 httpd: 4 image/jpeg 0
Nov  3 18:48:27 httpd: 5 Content-Length: 45842 0
Nov  3 18:48:27 httpd: 7  0
Nov  3 18:48:27 last message repeated 16 times
Nov  3 18:48:39 httpd: 2  0
Nov  3 18:48:40 httpd: 4 image/jpeg 0
Nov  3 18:48:40 httpd: 5 Content-Length: 124537 0
Nov  3 18:48:40 httpd: 7  0

И моя еще большая проблема заключается в том, что я не могу передать такие переменные, как $img или $cached, в обратный вызов, или я не могу установить переменную $length в обратном вызове для события STREAM_NOTIFY_FILE_SIZE_IS, а затем сравнить его с размером файла ($cached) в основном скрипте (я смог обнаружить несоответствие и удалить файл):

Nov  3 18:50:17 httpd: PHP Notice:  Undefined variable: length in /var/www/html/proxy.php on line 58
Nov  3 18:50:17 httpd: length=

У кого-нибудь есть решение моей проблемы?

Я просмотрел библиотеку PHP curl, но не понимаю, как она может мне здесь помочь.

Ниже приведен мой сценарий, для краткости я опустил проверки работоспособности URL:

<?php

define('MAX_SIZE', 1024 * 1024);
define('CACHE_DIR', '/var/www/cached_avatars/');

$img = urldecode($_GET['img']);

$opts = array(
        'http' => array(
                'method' => 'GET'
        )
);

$cached = CACHE_DIR . md5($img);

$finfo = finfo_open(FILEINFO_MIME);
$readfh = @fopen($cached, 'rb');
if ($readfh) {
        header('Content-Type: ' . finfo_file($finfo, $cached));
        header('Content-Length: ' . filesize($cached));

        while (!feof($readfh)) {
                $buf = fread($readfh, 8192);
                echo $buf;
        }

        fclose($readfh);
        finfo_close($finfo);
        exit();
}

$ctx = stream_context_create($opts);
stream_context_set_params($ctx, array('notification' => 'callback'));
$writefh = fopen($cached, 'xb');
$webfh = fopen($img, 'r', FALSE, $ctx);
if ($webfh) {
        $completed = TRUE;

        while (!feof($webfh)) {
                $buf = fread($webfh, 8192);
                echo $buf;
                if ($writefh)
                        fwrite($writefh, $buf);
        }

        fclose($webfh);
        if ($writefh)
                fclose($writefh);

        # XXX can't access $length in callback
        error_log('length=' . $length);

        # XXX can't access $completed in callback
        if (!$completed)
                unlink($cached);
}

function callback($code, $severity, $message, $message_code, $bytes_transferred, $bytes_total) {
        error_log(join(' ', array($code, $message, $message_code)));

        if ($code == STREAM_NOTIFY_PROGRESS && $bytes_transferred > MAX_SIZE) {
                exit('File is too big: ' . $bytes_transferred);

        } else if ($code == STREAM_NOTIFY_FILE_SIZE_IS) {
                if ($bytes_total > MAX_SIZE)
                        exit('File is too big: ' . $bytes_total);
                else {
                        header('Content-Length: ' . $bytes_total);
                        # XXX can't pass to main script
                        $length = $bytes_total;
                }

        } else if ($code == STREAM_NOTIFY_MIME_TYPE_IS) {
                if (stripos($message, 'image/gif') !== FALSE ||
                    stripos($message, 'image/png') !== FALSE ||
                    stripos($message, 'image/jpg') !== FALSE ||
                    stripos($message, 'image/jpeg') !== FALSE) {
                        header('Content-Type: ' . $message);
                } else {
                        exit('File is not image: ' . $mime);
                }
        } else if ($code == STREAM_NOTIFY_FAILURE) {
                $completed = FALSE;
        }
}

?>

Я не использую блокировку файлов в своем скрипте: чтение из кеша может возвращать неполный файл (потому что он все еще загружается) время от времени. Но я хочу, чтобы в моем кеше не было частично загруженных изображений. Также, если вы посмотрите на мой скрипт, я использую "xb", который должен предотвратить запись нескольких скриптов в 1 файл, поэтому одновременная запись здесь не проблема.


person Alexander Farber    schedule 03.11.2010    source источник


Ответы (1)


Библиотека curl - это то, что вы хотели бы, чтобы мы загружали изображение. Он обрабатывает тайм-ауты, перенаправления и проверку ошибок. Например, вы можете проверить ответ 404 (отсутствует файл) от сервера, к которому вы подключаетесь. Если все работает, то пишешь содержимое в файл кеша с помощью fopen.

$ch = curl_init();
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
curl_setopt($ch, CURLOPT_URL, $img_url);
$content = curl_exec($ch);
$info = curl_getinfo($ch);
$errorCode      = curl_errno($ch);
$errorMsg       = curl_error($ch);
curl_close($ch);
// Check for errors
if ( $errorCode==0 ) {
    // No connection errors, just for response type
    if ($info['http_code'] != 200) {
        // Something happened on the other side
        ...
    } else {
        // Image is in $content variable, save to cache file
        ...
    }
}
person Brent Baisley    schedule 03.11.2010
comment
Спасибо, но: 1) если я использую curl для чтения изображения в буфер, как мне сохранить низкое использование памяти? В моем исходном сценарии я читал 8192 фрагмента. 2) если я использую curl для чтения изображения непосредственно в файл, как мне убедиться, что файл не поврежден другим экземпляром скрипта? В моем исходном файле я использовал для этого fopen(..., xb). 3) Самое главное: как я могу убедиться, что curl не загружает слишком большие файлы, которые могут атаковать мой сервер с помощью DOS? Я не вижу способа установить ограничение на загружаемый файл при использовании curl (CURLOPT_RANGE — это просто рекомендация для сервера). - person Alexander Farber; 03.11.2010
comment
Вы устанавливаете CURLOPT_RETURNTRANSFER в false, тогда данные не будут переданы, только заголовки. Затем вы можете проверить заголовки на размер. Но в любом случае, PHP будет использовать только ограничение памяти, установленное в конфигурации (обычно 8 МБ), поэтому вы действительно не можете DOS-атака на свой сервер. - person Brent Baisley; 03.11.2010
comment
Спасибо, я попробую использовать CURL. Однако я столкнулся с проблемой cookie: stackoverflow.com/questions/4096470/ - person Alexander Farber; 04.11.2010