In file.inc, file_download() invokes all hook_file_download hooks to determine if a file is allowed to be transfered and set transfer headers. But there is no hook invoked if the transfer is allowed to proceed. For example, let's say I want to be able to keep file download statistics, but only if a transfer is allowed. If I implement a hook_file_download, I don't know if hooks invoked before or after my module will deny the transfer, and my statistics will be off.

I suggest introducing a new hook (e.g. hook_file_transferring or something more appropriate) which is invoked on all modules in file_download() just before the call to file_transfer():

  if (file_exists(file_create_path($filepath))) {
    $headers = module_invoke_all('file_download', $filepath);
    if (in_array(-1, $headers)) {
        return drupal_access_denied();
    }
    if (count($headers)) {
        module_invoke_all('file_transferring', $filepath, $headers);  // This is the new invocation
        file_transfer($filepath, $headers);
    }

It's up to people smarter than me to determine what return values from hook_file_transferring would be useful and appropriate.

My apologies for wasting your time if this has already been suggested or implemented.

Comments

webchick’s picture

Marked #346125: Add hook_file_downloaded() a duplicate of this.

j.somers’s picture

StatusFileSize
new1.69 KB

Attached is a really simple and basic attempt to implement this hook.

I opted to call it in the file_transfer() function because, in my humble opinion, the call is only required when the file is actually, and probably correctly, transferred. At least that's what I would expect. I passed the $source and $headers variables which are also passed to the file_transfer() function although, by heart, I can't really find where I would use the $headers.

The downside of placing this in the file_transfer() function is that the $source variably might get cluttered with the path to the files directory so that would be up to the user to extract the correct name of the original file and handle his actions accordingly.

j.somers’s picture

Status: Active » Needs review

Forgot to change the status.

drewish’s picture

The PHPDoc should probably just omit the @return if we don't return anything. Also you should come up with an example no matter how trivial.

dopry’s picture

It might even be fun to have a hook before the file goes out that allows headers and source to be modified.... I think you could trigger the completion after the file push is completed, but should be able to do that in hook_exit... oh wait file_transfer never triggers hook_exit.... maybe it should call something like drupal_page_footer... or invoke hook_exit itself....

dave reid’s picture

Issue tags: +D7FileAPIWishlist, +File API, +hooks

Very cool idea.

cwgordon7’s picture

Status: Needs review » Needs work
Issue tags: +Needs tests

This patch looks very good. However, it probably needs a short test before it's ready to be committed.

j.somers’s picture

I wanted to add a test to see whether the hook is called, but it seems that file_transfer is not called when calling file_download. Any pointers on how I should test this?

cwgordon7’s picture

Actually, file_transfer() IS called from file_download(). The following code is taken from api.drupal.org for the function file_download:

<?php
  if (file_exists(file_create_path($filepath))) {
    // Let other modules provide headers and controls access to the file.
    $headers = module_invoke_all('file_download', $filepath);
    if (in_array(-1, $headers)) {
      return drupal_access_denied();
    }
    if (count($headers)) {
      file_transfer($filepath, $headers);
    }
  }
?>

So as long as the file exists, and headers are returned (though none of them are -1), file_transfer() will be called. Perhaps this logic is broken - I really don't know enough about it to tell you.

j.somers’s picture

Status: Needs work » Needs review
StatusFileSize
new5.21 KB

The problem lies within the following code segment of file_transfer():

// Transfer file in 1024 byte chunks to save memory usage.
if ($fd = fopen($source, 'rb')) {
  while (!feof($fd)) {
    print fread($fd, 1024);
  }
  fclose($fd);
}
else {
  drupal_not_found();
}

In my original patch I placed the call to module_invoke_all() behind the fclose() call. If I place the call before the while (!feof($fd)) { call it seems to work. My guess is that the call to the hook is lost somewhere when printing to a buffer starts.

I modified my original patch and created a hook_file_transferring() hook which is called before the while loop starts.

eojthebrave’s picture

Status: Needs review » Needs work

There are a couple of minor issues with this patch.

Issues with white space at the end of at least one line.

+ // Check whether required hooks were called.

This line

+ * String specifying the file path to transfer.

Should probably read something like

"String specifying the path of the file to transfer"

Also, why are we calling this hook_file_transferring? To me that implies that the hook is called while the file is actually being transferred when in reality it is called just before the file is transfered. Why not hook_file_transfer, and then maybe also add a hook_file_transferred that gets called immediately after a successful file transfer.

cyberwolf’s picture

Subscribing.

aaron’s picture

Version: 7.x-dev » 8.x-dev

needs to be reworked a bit to work w/ stream uri's. also an api change, so switching to d8.

valthebald’s picture

Component: file system » file.module
Status: Needs work » Needs review
Issue tags: -D7FileAPIWishlist
StatusFileSize
new3.85 KB

Rerolled patch against 8.x

marcingy’s picture

Status: Needs review » Needs work
+ * Act on begin of file transfer.
+ *

Should be Act on begining of file transfer I assume

valthebald’s picture

Status: Needs work » Needs review
StatusFileSize
new3.86 KB

Fixed

Status: Needs review » Needs work
Issue tags: -File API, -Needs tests, -hooks

The last submitted patch, hook-file-transferring-233997-16.patch, failed testing.

marcingy’s picture

Status: Needs work » Needs review

dave reid’s picture

Issue summary: View changes
Issue tags: +Media Initiative

Interesting hook and I think it could be very useful. I think it should probably be named hook_file_transfer() though.

dave reid’s picture

Looks like #1561362: Change file_transfer() to use BinaryFileResponse removed file_transfer() in favor of Symfony\Component\HttpFoundation\BinaryFileResponse. I don't think a hook is possible anymore.

dave reid’s picture

Version: 8.x-dev » 7.x-dev
Component: file.module » file system

Actually, this could still be nice to have to Drupal 7.

dave reid’s picture

StatusFileSize
new3.66 KB

Re-rolled for Drupal 7 and renamed to hook_file_transfer().

dave reid’s picture

StatusFileSize
new4.02 KB

Forgot to re-add the test assertion.

Status: Needs review » Needs work

The last submitted patch, 24: 233997-hook-file-transfer.patch, failed testing.

dave reid’s picture

Status: Needs work » Needs review
StatusFileSize
new4.31 KB

Added 'load' to the expected hooks to have run.

dave reid’s picture

maximpodorov’s picture

I suggest to trigger hook just before sending file to the client.

andypost’s picture

I'd prefer to have this kind of issue as part of distribution or patch in the issue
Only sites that need to count any metrics for private file downloads needs this hooks
Otherwise hook_menu_alter will be enough for other 99%

+++ b/includes/file.inc
@@ -1952,6 +1952,8 @@ function file_unmanaged_save_data($data, $destination = NULL, $replace = FILE_EX
 function file_transfer($uri, $headers) {
+  module_invoke_all('file_transfer', $uri, $headers);

so when someone will need to count files that delivered per sent... core will add another hook?

slashrsm’s picture

Issue tags: +sprint
slashrsm’s picture

Issue tags: +D8Media
slashrsm’s picture

Issue tags: -D8Media
Chewits’s picture

Any chances this will be integrated to D7 ever?
Dave, what do you think about the comments by maximpodorov and andypost?

Chewits’s picture

All I need is to count private downloads. I need rough figures of initiated transfers. I don't want to count successful transfers or detect power line breaks, or something else.
One of my contrib modules already implements hook_menu_alter() for 'system/files' path but still allows to take advantage of hook_file_download().

Could someone suggest, wouldn't this be a good idea to implement hook_file_download() together with hook_module_implements_alter() in my custom module.
- hook_file_download() will do the job, e.g. watchdog(...);
- hook_module_implements_alter() will take care of my hook_file_download() to be triggered in the last turn.

I'll also add some 'weight' to that module for more reliability.
I think it's almost the same that is provided in 233997-hook-file-transfer.patch, but without patching the core. Am I right?

David_Rothstein’s picture

Version: 7.x-dev » 8.0.x-dev
Issue tags: +Needs backport to D7

@Chewits, that sounds like it would work in Drupal 7 (since https://api.drupal.org/api/drupal/includes!file.inc/function/file_downlo... stops invoking additional modules' hooks once it knows that a download won't be allowed). Probably wouldn't work in Drupal 8.

It seems to me like the hook requested here could certainly be implemented in https://api.drupal.org/api/drupal/core!modules!system!src!FileDownloadCo... or similar in Drupal 8 (which matches the original issue proposal here, to have it in file_download() in Drupal 7).

For Drupal 7, I'm not sure which is better - file_transfer() is more generic, but also lower-level and gets called randomly in other scenarios (for example, image_style_deliver() calls it for image derivatives, but only when a derivative is delivered by Drupal the first time it's generated) so implementations of the hook would have to be careful to filter those other cases out if they are e.g. just trying to count private file downloads.

maximpodorov’s picture

Repeating of #28:
I suggest to trigger hook just before sending file to the client.

David_Rothstein’s picture

@maximpodorov, that's not a very specific suggestion - do you think it should be in file_download() or file_transfer()? If we invoke it from file_download() right before file_transfer() is called then the file transfer is essentially guaranteed to happen.

maximpodorov’s picture

Here is my patch from #2500949: Add hook_file_transfer to allow modules to react on private file downloads (it's for D7):

diff --git a/includes/file.inc b/includes/file.inc
index d3ac87e..92b1469 100644
--- a/includes/file.inc
+++ b/includes/file.inc
@@ -1964,6 +1964,7 @@ function file_transfer($uri, $headers) {
   $scheme = file_uri_scheme($uri);
   // Transfer file in 1024 byte chunks to save memory usage.
   if ($scheme && file_stream_wrapper_valid_scheme($scheme) && $fd = fopen($uri, 'rb')) {
+    module_invoke_all('file_transfer', $uri, $headers, $scheme);
     while (!feof($fd)) {
       print fread($fd, 1024);
     }

Version: 8.0.x-dev » 8.1.x-dev

Drupal 8.0.6 was released on April 6 and is the final bugfix release for the Drupal 8.0.x series. Drupal 8.0.x will not receive any further development aside from security fixes. Drupal 8.1.0-rc1 is now available and sites should prepare to update to 8.1.0.

Bug reports should be targeted against the 8.1.x-dev branch from now on, and new development or disruptive changes should be targeted against the 8.2.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Version: 8.1.x-dev » 8.2.x-dev

Drupal 8.1.9 was released on September 7 and is the final bugfix release for the Drupal 8.1.x series. Drupal 8.1.x will not receive any further development aside from security fixes. Drupal 8.2.0-rc1 is now available and sites should prepare to upgrade to 8.2.0.

Bug reports should be targeted against the 8.2.x-dev branch from now on, and new development or disruptive changes should be targeted against the 8.3.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Version: 8.2.x-dev » 8.3.x-dev

Drupal 8.2.6 was released on February 1, 2017 and is the final full bugfix release for the Drupal 8.2.x series. Drupal 8.2.x will not receive any further development aside from critical and security fixes. Sites should prepare to update to 8.3.0 on April 5, 2017. (Drupal 8.3.0-alpha1 is available for testing.)

Bug reports should be targeted against the 8.3.x-dev branch from now on, and new development or disruptive changes should be targeted against the 8.4.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Version: 8.3.x-dev » 8.4.x-dev

Drupal 8.3.6 was released on August 2, 2017 and is the final full bugfix release for the Drupal 8.3.x series. Drupal 8.3.x will not receive any further development aside from critical and security fixes. Sites should prepare to update to 8.4.0 on October 4, 2017. (Drupal 8.4.0-alpha1 is available for testing.)

Bug reports should be targeted against the 8.4.x-dev branch from now on, and new development or disruptive changes should be targeted against the 8.5.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Version: 8.4.x-dev » 8.5.x-dev

Drupal 8.4.4 was released on January 3, 2018 and is the final full bugfix release for the Drupal 8.4.x series. Drupal 8.4.x will not receive any further development aside from critical and security fixes. Sites should prepare to update to 8.5.0 on March 7, 2018. (Drupal 8.5.0-alpha1 is available for testing.)

Bug reports should be targeted against the 8.5.x-dev branch from now on, and new development or disruptive changes should be targeted against the 8.6.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Version: 8.5.x-dev » 8.6.x-dev

Drupal 8.5.6 was released on August 1, 2018 and is the final bugfix release for the Drupal 8.5.x series. Drupal 8.5.x will not receive any further development aside from security fixes. Sites should prepare to update to 8.6.0 on September 5, 2018. (Drupal 8.6.0-rc1 is available for testing.)

Bug reports should be targeted against the 8.6.x-dev branch from now on, and new development or disruptive changes should be targeted against the 8.7.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Version: 8.6.x-dev » 8.8.x-dev

Drupal 8.6.x will not receive any further development aside from security fixes. Bug reports should be targeted against the 8.8.x-dev branch from now on, and new development or disruptive changes should be targeted against the 8.9.x-dev branch. For more information see the Drupal 8 and 9 minor version schedule and the Allowed changes during the Drupal 8 and 9 release cycles.

Version: 8.8.x-dev » 8.9.x-dev

Drupal 8.8.7 was released on June 3, 2020 and is the final full bugfix release for the Drupal 8.8.x series. Drupal 8.8.x will not receive any further development aside from security fixes. Sites should prepare to update to Drupal 8.9.0 or Drupal 9.0.0 for ongoing support.

Bug reports should be targeted against the 8.9.x-dev branch from now on, and new development or disruptive changes should be targeted against the 9.1.x-dev branch. For more information see the Drupal 8 and 9 minor version schedule and the Allowed changes during the Drupal 8 and 9 release cycles.

Version: 8.9.x-dev » 9.2.x-dev

Drupal 8 is end-of-life as of November 17, 2021. There will not be further changes made to Drupal 8. Bugfixes are now made to the 9.3.x and higher branches only. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

Version: 9.2.x-dev » 9.3.x-dev

Version: 9.3.x-dev » 9.4.x-dev

Drupal 9.3.15 was released on June 1st, 2022 and is the final full bugfix release for the Drupal 9.3.x series. Drupal 9.3.x will not receive any further development aside from security fixes. Drupal 9 bug reports should be targeted for the 9.4.x-dev branch from now on, and new development or disruptive changes should be targeted for the 9.5.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

smustgrave’s picture

Status: Needs review » Postponed (maintainer needs more info)

This ticket has been around for a really long time. Wonder if its still valid? looks like it still needs a fair amount of work and issue summary update.

Version: 9.4.x-dev » 9.5.x-dev

Drupal 9.4.9 was released on December 7, 2022 and is the final full bugfix release for the Drupal 9.4.x series. Drupal 9.4.x will not receive any further development aside from security fixes. Drupal 9 bug reports should be targeted for the 9.5.x-dev branch from now on, and new development or disruptive changes should be targeted for the 10.1.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

smustgrave’s picture

Status: Postponed (maintainer needs more info) » Closed (outdated)

If still a valid request please reopen.

Thanks!