Alexander Farber
Alexander Farber

Reputation: 23008

PHP: Detecting fopen() failures when downloading images

I have a card game (screenshot below) in which I display player avatars.

For the avatars I've written a short proxy.php script, which would take an image URL passed to it as ?img= parameter, download it and save under /var/www/cached_avatars/md5_of_that_url at my CentOS 5 machine. Next time the script is called with the same URL, it will find that image in the dir and serve it directly to STDOUT.

This works mostly well, but for some avatars the initial download fails (I suppose it times out) and you don't see the lower part of the player picture:

alt text

I'd like to detect this image download failure and delete the cached partial file, so that it is re-downloaded on the next proxy.php call.

I've tried detecting STREAM_NOTIFY_FAILURE or STREAM_NOTIFY_COMPLETED events in my callback, but they are not fired. The only events I see are: 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

And my even bigger problem is, that I can't pass variables like $img or $cached into the callback or I can't set a $length variable in the callback on a STREAM_NOTIFY_FILE_SIZE_IS event and then compare it with filesize($cached) in the main script (I could detect the mismatch and delete the file):

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=

Does anybody have a solution for my problem?

I've looked at the PHP curl library, but don't see how could it help me here.

Below is my script, I've omitted the URL sanity checks for brevity:

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

?>

I don't use any file locking in my script: it's ok for a read from cache to return an incomplete file (because it is still being downloaded) once in a while. But I want to keep my cache free of any partially downloaded images. Also if you look at my script I use "xb" which should prevent from several scripts writing into 1 file, so this simultaneous writing is not a problem here.

Upvotes: 0

Views: 1197

Answers (1)

Brent Baisley
Brent Baisley

Reputation: 12721

The curl library is what you would want to us to download the image. It handles timeouts, redirects and error checking. For example, you can check for a 404 (missing file) response from the server you are connecting to. If everything works, then you write the contents to a cache file using 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
        ...
    }
}

Upvotes: 1

Related Questions