There is a bug in Chrome which means that, under some circumstances, an image can be displayed on the page for a moment and then disappear.
http://www.google.com/support/forum/p/Chrome/thread?tid=1d825178248ab136&hl=en
The fix is to remove the Content-Length header. Unfortunately that is virtually impossible to do in Drupal without hacking core.
Going into this problem revealed an interesting omission: the file_download() function collects headers from interested modules, but if two modules offer the same header the earlier one gets overwritten. Heavier modules can overwrite the headers of lighter modules.
So making my module heavy would mean that I could overwrite Content-Length with NULL.
But this is not a good solution. What's missing is that, after collecting the headers, file_download() does not use drupal_alter() to allow modules to change the headers.
So the correct solution is a patch which inserts drupal_alter('file_download_headers', $headers, $uri) into the file_download() function at the correct location. And you can find the patch here:
http://drupal.org/node/1137534
All I had to do then was create a function that implemented hook_file_download_headers() which took a look at the $uri and, if it's a graphic, remove the Content-Length header, which is perfectly safe because it's not a critical header.
Showing posts with label file_download. Show all posts
Showing posts with label file_download. Show all posts
Thursday, 28 April 2011
Wednesday, 30 March 2011
Using stream wrappers
Sorry I haven't uploaded my two new modules to d.o yet - just not had time.
In my current contract the client wanted the option of either downloading a PDF or opening it in the browser (assuming the browser could handle it - but that's not my problem). These PDFs being stored in the Private file space.
The first question they had was: can it be done? They'd been told it couldn't. But I assured them with naive certainty that it probably could. I had already sorted out the ability to auto-create and download archives and I was pretty sure it was merely a matter of the correct HTTP headers.
But how to do it? What I needed was a different path to the same file, one path would download it, the other path would display it in the browser.
If you're using Private files the URL you get is 'system/files/filename.pdf' which goes through various checks to ensure the download is permitted, and gets the HTTP headers at the same time, using hook_file_download(). The path fragment "system/files" is what invokes the file_download() function. If you create a different stream wrapper, such as my archive stream, this is replaced with "system/archive" - or whatever you want really.
So I thought, okay, how about I create a new stream wrapper based on a "system/views" path which invokes file_download() but extracts the Content-Disposition header before sending it.
The trick here is to make the new stream wrapper point to the same location as the Private stream - so it can access the same files - and then, when I'm building the download link, I get the file's URL (e.g. "system/files/filename.pdf") and simply do a string replace, changing "files" to "views".
So the user clicks on my modified URL, it goes through my routines which get the headers but strip out the Content-Disposition header and, as a result, instead of downloading the file, it opens in the browser. I have two links to the same file, each one does something different with the file.
In retrospect using the word "Views" may be a little confusing, obviously it's nothing to do with the Views module - sorry about that.
You need the stream wrapper class...
class ViewsStreamWrapper extends DrupalLocalStreamWrapper {
/**
* Implements abstract public function getDirectoryPath()
*
* This is identical to the private stream wrapper which means
* we can just replace 'files' with 'views' the path and get the same file
*/
public function getDirectoryPath() {
return variable_get('file_private_path', '');
}
/**
* Overrides getExternalUrl().
*/
public function getExternalUrl() {
$path = str_replace('\\', '/', $this->getTarget());
return url('system/views/' . $path, array('absolute' => TRUE));
}
}
You need to tell Drupal about the class...
/**
* Implements hook_stream_wrappers().
*/
function views_stream_stream_wrappers() {
return array(
'views' => array(
'name' => t('Views'),
'class' => 'ViewsStreamWrapper',
'description' => t('Stream wrapper for viewing files in a browser'),
'type' => STREAM_WRAPPERS_READ,
),
);
}
And then the hook_menu() and hook_file_download() support, note this code assumes a PDF.
/**
* Implements hook_menu().
*/
function views_stream_menu() {
$items = array();
$items['system/views'] = array(
'title' => 'View files',
'page callback' => 'file_download',
'page arguments' => array('views'),
'access callback' => TRUE,
'type' => MENU_CALLBACK,
);
return $items;
}
/**
* Implements hook_file_download().
*
* Construct the headers to ensure the file gets downloaded
*/
function views_stream_file_download($uri) {
list($scheme, $path) = explode('://', $uri, 2);
if ($scheme=='views') {
$file = (object) array(
'filename' => basename($path),
'filemime' => 'application/pdf',
'filesize' => filesize(drupal_realpath($uri)),
);
$headers = file_get_content_headers($file);
unset($headers['Content-Disposition']);
return $headers;
}
}
And then as an example of modifying a private file. The $uri variable contains the file's internal path which you can get from a file entity, or file field.
$href = file_create_url($uri);
$href = str_replace('/files/', '/views/', $href);
$link = array(
'#type' => 'link',
'#title' => t('View'),
'#href' => $href,
'#attributes' => array('target' => '_blank'),
'#weight' => 0,
);
And that's all there is to it.
In my current contract the client wanted the option of either downloading a PDF or opening it in the browser (assuming the browser could handle it - but that's not my problem). These PDFs being stored in the Private file space.
The first question they had was: can it be done? They'd been told it couldn't. But I assured them with naive certainty that it probably could. I had already sorted out the ability to auto-create and download archives and I was pretty sure it was merely a matter of the correct HTTP headers.
But how to do it? What I needed was a different path to the same file, one path would download it, the other path would display it in the browser.
If you're using Private files the URL you get is 'system/files/filename.pdf' which goes through various checks to ensure the download is permitted, and gets the HTTP headers at the same time, using hook_file_download(). The path fragment "system/files" is what invokes the file_download() function. If you create a different stream wrapper, such as my archive stream, this is replaced with "system/archive" - or whatever you want really.
So I thought, okay, how about I create a new stream wrapper based on a "system/views" path which invokes file_download() but extracts the Content-Disposition header before sending it.
The trick here is to make the new stream wrapper point to the same location as the Private stream - so it can access the same files - and then, when I'm building the download link, I get the file's URL (e.g. "system/files/filename.pdf") and simply do a string replace, changing "files" to "views".
So the user clicks on my modified URL, it goes through my routines which get the headers but strip out the Content-Disposition header and, as a result, instead of downloading the file, it opens in the browser. I have two links to the same file, each one does something different with the file.
In retrospect using the word "Views" may be a little confusing, obviously it's nothing to do with the Views module - sorry about that.
You need the stream wrapper class...
class ViewsStreamWrapper extends DrupalLocalStreamWrapper {
/**
* Implements abstract public function getDirectoryPath()
*
* This is identical to the private stream wrapper which means
* we can just replace 'files' with 'views' the path and get the same file
*/
public function getDirectoryPath() {
return variable_get('file_private_path', '');
}
/**
* Overrides getExternalUrl().
*/
public function getExternalUrl() {
$path = str_replace('\\', '/', $this->getTarget());
return url('system/views/' . $path, array('absolute' => TRUE));
}
}
You need to tell Drupal about the class...
/**
* Implements hook_stream_wrappers().
*/
function views_stream_stream_wrappers() {
return array(
'views' => array(
'name' => t('Views'),
'class' => 'ViewsStreamWrapper',
'description' => t('Stream wrapper for viewing files in a browser'),
'type' => STREAM_WRAPPERS_READ,
),
);
}
And then the hook_menu() and hook_file_download() support, note this code assumes a PDF.
/**
* Implements hook_menu().
*/
function views_stream_menu() {
$items = array();
$items['system/views'] = array(
'title' => 'View files',
'page callback' => 'file_download',
'page arguments' => array('views'),
'access callback' => TRUE,
'type' => MENU_CALLBACK,
);
return $items;
}
/**
* Implements hook_file_download().
*
* Construct the headers to ensure the file gets downloaded
*/
function views_stream_file_download($uri) {
list($scheme, $path) = explode('://', $uri, 2);
if ($scheme=='views') {
$file = (object) array(
'filename' => basename($path),
'filemime' => 'application/pdf',
'filesize' => filesize(drupal_realpath($uri)),
);
$headers = file_get_content_headers($file);
unset($headers['Content-Disposition']);
return $headers;
}
}
And then as an example of modifying a private file. The $uri variable contains the file's internal path which you can get from a file entity, or file field.
$href = file_create_url($uri);
$href = str_replace('/files/', '/views/', $href);
$link = array(
'#type' => 'link',
'#title' => t('View'),
'#href' => $href,
'#attributes' => array('target' => '_blank'),
'#weight' => 0,
);
And that's all there is to it.
Subscribe to:
Posts (Atom)