I have a content type with a (private) file field. Content is protected by a node access module.

I would like to log (in a particular way) when a user is about to download a file.

hook_file_download looks like it will do the trick, but on the other hand, its stated purpose is to "control access". I'm letting Drupal Core control access; I just want to log a (successful) download.

Is there a better hook to do this with?

  • 3
    there isn't a D7 version out there, but drupal.org/project/filefield_stats is a great module for D6, and maybe some of its code could be of use to you if you are already writing a module...
    – Jimajamma
    Commented Apr 5, 2012 at 17:45
  • yeah, good tip, thanks. Basically that module makes a page callback function to deal with it. Had hoped there was a more graceful way to compliment the D7 private/'managed' files API Commented Apr 5, 2012 at 19:07
  • Just by the way, for public files it could be easily done at webserver level. Sadly won't work for private.
    – Mołot
    Commented Sep 8, 2015 at 21:46

1 Answer 1


hook_file_download() is invoked in two occasions:

  • When Drupal is verifying the users have access to the file they are trying to download

    function file_download_access($uri) {
      return count(file_download_headers($uri)) > 0;
  • When Drupal is returning the headers for the file to download (and then the data for the file to download)

    function file_download() {
      // Merge remainder of arguments from GET['q'], into relative file path.
      $args = func_get_args();
      $scheme = array_shift($args);
      $target = implode('/', $args);
      $uri = $scheme . '://' . $target;
      if (file_stream_wrapper_valid_scheme($scheme) && file_exists($uri)) {
        $headers = file_download_headers($uri);
        if (count($headers)) {
          file_transfer($uri, $headers);
      else {

In both the cases, the function invoking the hook is file_download_headers().

function file_download_headers($uri) {
  // Let other modules provide headers and control access to the file.
  // module_invoke_all() uses array_merge_recursive() which merges header
  // values into a new array. To avoid that and allow modules to override
  // headers instead, use array_merge() to merge the returned arrays.
  $headers = array();
  foreach (module_implements('file_download') as $module) {
    $function = $module . '_file_download';
    $result = $function($uri);
    if ($result == -1) {
      // Throw away the headers received so far.
      $headers = array();
    if (isset($result) && is_array($result)) {
      $headers = array_merge($headers, $result);
  return $headers;

Watching the Drupal code, I cannot assure the hook is always called twice in row, during the same page request; if it were so, you could take the necessary action when the hook is invoked for the second time.

I would rather change the callbacks for $directory_path . '/styles/%image_style' (where $directory_path contains the value returned from file_stream_wrapper_get_instance_by_scheme('public')->getDirectoryPath()), system/files/styles/%image_style, system/files, and system/temporary to verify when a file is going to be downloaded, and log what happening.

This is not going to change in Drupal 8, as it doesn't split hook_file_download() in two hooks.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.