drupal_page_cache_header() has some nice functionality to abort page loads with a '304 Not Modified' response if the page is in the cache.
Unfortunately, private file downloads can't have the same - a module implementing hook_file_download() would need to copy and adapt part of drupal_page_cache_header() and exit inside the hook, which is awful. Neither can modules like imagecache which feature their own download paths make use of Last-Modified headers without duplicating code from drupal_page_cache_header() in the same way.
Therefore, this patch takes an approach by fago as inspiration and splits out the shareable parts from drupal_page_cache_header() into a new function drupal_apply_cache_headers() which can be used by file.inc, imagecache, or whatever download module wants to use it. The code semantics for drupal_page_cache_header() stay the same except that the Last-Modified header is added also when doing the 304 - that's necessary for modules that don't want to use the page creation time as Last-Modified (makes little sense for files, arguably). For drupal_page_cache_header() generated headers, adding Last-Modified should not make any difference in behaviour.
As example application of that new function (which should be quite beneficial to private download performance as well), I used it in file_download() after we know that a file will be downloaded. I also replaced file_exists() with is_file() as the former might also retrieve directories which we rather don't want to pass to file_transfer().
| Comment | File | Size | Author |
|---|---|---|---|
| #7 | drupal-factor-out-cache-headers-try3.patch | 5.14 KB | jpetso |
| #4 | drupal-factor-out-cache-headers-try2.patch | 5.03 KB | jpetso |
| drupal-factor-out-cache-headers.patch | 5.02 KB | jpetso |
Comments
Comment #1
jpetso commentedNicer title. Well, nitpicking... you know.
Comment #2
drewish commentedsubscribing.
Comment #3
jpetso commented@drewish: This patch has been sitting idle in the queue for nearly two months... any chance you could spare a bit of time to test & review it?
Comment #4
jpetso commentedRerolled so that it applies to current HEAD again, using the newly added space for the concatenation operator. Unchanged otherwise.
Comment #5
drewish commentedComment #6
c960657 commentedUnder what cirumstances will stat() return false? A few lines above you just verified that $full_filepath is a file, and this wont change even if the file is deleted in the meantime due to the stat cache, unless some module calls clearstatcache(). A comment explaining the additional check would be helpful.
Also, why not just use filemtime()? That would make the code easier to read.
+function drupal_apply_cache_headers(&$headers, $last_modified_timestamp) {It seems like a quite unusual approach to modify array specified as a function argument rather than returning the necessary headers or just setting them directly using header(). Any particular reason to use this pattern?
It appears as if drupal_set_header() is the "official" way of setting headers rather than calling header() directly.
Comment #7
jpetso commented@c960657: Let's start from the bottom of your comment.
drupal_set_header() is indeed the "official" way of setting headers. So header() should only be used if drupal_set_header() is not yet available, which is during the bootstrap. drupal_page_cache_header() used header() even before my patch, which is because it's invoked during the "late page cache" bootstrapping phase. drupal_set_header() on the other hand is in common.inc, which is only loaded during the last, "full" bootstrapping phase. So we can't use drupal_set_header() there.
Once Drupal is bootstrapped, we do want to use it though, so it would be a bad idea to call either header() or drupal_set_header() directly within drupal_apply_cache_headers() - except for the case when the function exits right away, and in that case the drupal_set_header() functionality is not needed. So in case the function does not exit, we want to enable the caller to use the appropriate header setter function, which is probably drupal_set_header() for everyone but the page cache itself.
As for passing the $headers array by reference, changing that to a simple array_merge() would not make any difference in practice. It's just that if the function possibly exit()s, I'd find it strange to have that happen in an assignment statement, i.e.
That's probably a taste issue, I think... drewish, any comments on that matter?
filemtime() is a good suggestion, I was not aware that there is a specialized function for that. And finally,
I have no idea, and frankly, I don't care. The API documentation for stat() and filemtime() says,
It does not specify under which circumstances it might return FALSE, which is reason enough not to assume that it's always going to work. Hence the check. A bit of defensive coding doesn't hurt here, or does it?
Find attached a rerolled version using filemtime(). Patch rolled right after the file.inc cleanup patch (#308434: Clean up file.inc ahead of hook_file and add unit tests.) was applied.
Comment #8
c960657 commentedNo, definitely not. My thought was just that if FALSE indicates that the file has been deleted by another thread, we might as well skip the file_transfer() call as well and instead return a 404 status. In other words, try to handle the real problem rather than just the symptom. But you are right that the PHP manual isn't very specific about what FALSE actually means, so your suggested behaviour is probably the best we can do.
Your other comments to my comments sounds convincing to me :-) I agree that exit()'ing in an assignment would probably be too weird.
Comment #9
Anonymous (not verified) commentedThe last submitted patch failed testing.
Comment #11
joseph.olstadI suggest closing this issue in favour of
#1715222: Remove almost all file_exists() and is_file() calls from core
and
#752730: Remove file_exists() during bootstrap