Drupal coder

Mixing private and public downloads in Drupal 6

It's a well know fact that Drupal 6 has built in support for private downloads. For those of you that don't, enabling the private downloads feature on Administer > Settings > Site configuration puts Drupal back into control when it comes to file downloads. Enabling this option all file requests are handled via Drupal. So Drupal can do download counting, access checks, ... In the case of public downloads, Drupal never knows when a file (in the files directory) is requested, since your web server is handling these on its own.

A problem in Drupal (now) though, is that you have too choose between one or the other. By default you can't use the private downloads and public downloads method at the same time. "By default" I said, because there's a very simple way to mix both.

Suppose you want to protect some directory in your files folder so its files are only visible to users of a certain role. Let's call that directory 'privatedownloads'. Create a .htaccess file in that folder and put in the following content:

<IfModule mod_rewrite.c>
  RewriteEngine on
  RewriteBase /system/files/privatedownloads
  RewriteRule ^(.*)$ $1 [L,R=301]
</IfModule>

This will redirect all calls to this folder to a new path, system/files/privatedownloads. So a call to files/privatedownloads/test.jpg will be redirected to system/files/privatedownloads/test.jpg.

Since this path now doesn't physically exist on your server (it shouldn't), the request will be passed to Drupal. Let's respond to it by implementing hook_menu.

/**
 * Implementation of hook_menu().
 */
function your_module_name_menu() {
  $items['system/files/privatedownloads'] = array(
    'access arguments' => array('access private downloads folder'),
    'type' =>  MENU_CALLBACK,
    'page callback' => 'file_download',
    'page arguments' => array('privatedownloads'),
  );
  return $items;
}

As you see, we're passing of the request to file_download, a function that is available in Drupal core and which is normally used for Drupal's default implementation of private downloads. I've also attached a permission to this callback, called access private downloads folder. Now you can go to your permissions page and set this permission for the desired roles.

This all works fine now (we're getting the correct file and the permission are at work), only images aren't displayed as images, pdf files aren't displayed in a pdf reader etc. That's due to an incorrect mimetype. Let's set the correct mimetype using hook_file_download and we're done.

/**
 * Implementation of hook_file_download().
 */
function your_module_name_file_download($file) {
  $info = image_get_info(file_create_path($file));
  return array('Content-type: '. file_get_mimetype($file));
}
February 09, 2009Drupal, file management, tutorial

Comments

Brilliant!
Saved me from having to move an entire site to private downloads.
I'll use the module inspired in this article (private_download)
Thanks!

Hi to all!

I,m trying to find a solution to restrict the Webform file submission (without using module dedicated due to is no longer supported and updated).

I try with a custom module used for File field but result is:

1) Files are protected (Anonymous get an Access denied page)
2) But, with correct permission, nobody can download them (registred user get a Page Not Found)

Any Ideas about?

Thanks!

i got this working, but its only working with
www.example.com

when i test this with

example.com (without www)
or with the [IP address] directly, then the file is SHOWN nevertheless, even though it should be protected.
what to i do wrong?

Can I access the page arguments in the method "mymodule_file_download($file)" somehow?

I have multiple folders with content to protect and have do distinguish between different types.

Who can help?

Very elegant solution! Saved me a lot of time.

i was try and try and get it...!!

Just on the off chance, I have tried it again and it is now working.

I have no idea why it wasn't working and why it is working now. I didn't change anything.

Anyway thanks and still any thoughts or insights would be very appreciated.

This module works great on my local server but when I try it under bluehost it gives me access denied like it should when I am not logged on and gives me page not found when I do log on.

Bluehost does allow mod_rewrite.

Any ideas what I should look for to fix the problem.

Thanks

THIS GOT ME - IT IS LEFT OUT OF THE POST

You have to implement hook_perm

function your_module_perm() {

return array('your permission');

}

Works great for downloading files. Thanks a lot.
Is there a (simple) way to modify the hook_menu function so that I can display the files in the browser instead of downloading? Because I need to protect html help files and its not possible to download those. I want to access the file in a blank page, if thats possible. Any ideas?
Thank u very much.

Does anyone know if this will work with the Download Count module? Since it's going through Drupal's system, I'm assuming it can keep track of download count etc.. Thanks.

This is exactly what I've been looking for. I tried the Private Upload module, but it just made things too complicated. Private files suddenly were public. Drupal's private system slowed down my site, specially with the images.

This info really saved the day for me. Special thanks to John H for clarifying the whole thing.

Sorry! Disregard my last comment—figured it out... *chagrined*

I'm a newbie at php, and drupal. I made two filefields—one for private files and one for public files and they both go to different directories. I've tried implementing the code above, but nothing seems to work... I tried a .module file with a .htaccess file in the protected folder. I tried putting all the code above into a .htaccess file, but nothing seems to work. Could you be more explicit as to how and where to put the above code—in extremely simple steps? I'm so confused; please help.

Hi! I have installed the private download module. If I login into drupal and click on the link to download the private file, I am redirected to "http://www.mydomain.com/system/files/protected/file.pdf" and the message is displayed "page not found".
Why is not the file displayed? Does the hook method not work? Thanks, Christoph

For those doing this with sub-folders (for example, in a folder located at "sites/default/files/products/privatedownload" instead of just "sites/default/files/privatedownload"):

In all of the code in this post, privatedownload is the relative name of the directory you're protecting. It's relative to sites/default/files. So, as I said, if your folder were sites/default/files/products/privatedownload, the following needs to happen:

In the .htaccess file inside your protected folder "privatedownload":
RewriteBase /system/files/products/privatedownloads

Notice how we added the parent directory, 'products', to our RewriteBase command.

In hook_menu in your custom module, the menu item path:
$items['system/files/products/privatedownloads']

As mentioned in one of the comments below, the Drupal menu path should match the path you put in your .htaccess folder (minus the leading slash, of course).

In hook_menu in your custom module, the menu item page arguments:
'page arguments' => array('products/privatedownloads'),

Notice how even the page argument requires us to put the parent directory in there. Otherwise the file_download function goes to look for our file under "sites/default/files/privatedownloads" and not the correct location of "sites/default/files/products/privatedownloads".

As a use-case, I'm using CCK filefield with specific paths for each field I've defined, and I limit access to these uploads to different roles. Due to this complexity, I like to keep the uploaded files organized in a directory hierarchy on the server. With this solution, you can have files located in multiple folders, restricted to multiple roles, and not have all those folders living directly under "sites/default/files".

@Patrick
thanks so much for your module, it's just what I was looking for!!
just a quick note though, in my case I had to add an extra line before the rewrite rule (perhaps because I am running Drupal on a shared host):
---
RewriteBase /
---

@Craig
I am using filefield_paths with no problems so far. Be aware I did very superficial testing though.

I wrote this module based of the PrivateDownload module. It used to work but suddenly it says "Page not found" instead of displaying the content. If i'm not loged on It says 'Access Denied' so I know at least it's using the permisions but I don't know why it's not fixing the URL any more.

//-------------------------------------------------------------------
//Name: folder_protector_menu
//Abstract: Tells Drupal What Sites I'm Useing
//-------------------------------------------------------------------
function folder_protector_menu()
{

$items = array();
$VariableValue = "";

$items['admin/settings/folderprotector'] = array(
'title' => 'ProtectFolders',
'description' => 'Protect Folders',
'page callback' => 'drupal_get_form',
'page arguments' => array('folder_protector_admin'),
'access arguments' => array('Protect Folders'),
'type' => MENU_NORMAL_ITEM,
);

//Get A List Of Variables That Hold The Folder Names
$query_result = folder_protector_GetVariables();

//Loop threw Recordset
while ($Items = db_fetch_object($query_result))
{
//The Value is stored with other formating that needs to be striped out
$VariableValue = folder_protector_StripFormating($Items->value);

//$items['files/system/%'] = array(
//'access arguments' => array('access private download directory'),
//'page callback' => 'file_download',
//'page arguments' => array(variable_get('private_download_directory', 'private')),
//'type' => MENU_CALLBACK,

$items['files/system/private/'.$VariableValue.'/%' ] = array(
'access arguments' => array('Can Access The '.$VariableValue.' Folder'),
'page callback' => 'file_download',
'page arguments' => array($VariableValue),
'type' => MENU_CALLBACK
);
}

return $items;
}

//-------------------------------------------------------------------
//Name: folder_protector__file_download
//Abstract: Fix Mime Type
//-------------------------------------------------------------------
function folder_protector__file_download($file)
{
$header = array('Content-Type: '. file_get_mimetype($file));
// add additional file header attributes
return array_merge($header, explode("\n", variable_get('folder_protector_header', "Content-Transfer-Encoding: binary\nCache-Control: max-age=60, must-revalidate")));

}

I downloaded and installed private download module but when I try and configure it under 'administer', 'site config', 'private download' and I enter my 'file system path' for/in htacces content I keep getting the error 'The RewriteBase path does not equal /system/files/private in htaccess content.'

I have tried various combinations but no luck. I do notice that the 'private' folder is being created but the htaccess is no being written out. Any suggestions appreciated.

Thank you ...Neil

1) Correct. File access is based on node access. How you restrict node access is a different question. It works with organic groups, but should also work with other node access modules.
2) No idea. Test it. My module does not do anything to the database, so nothing should break, if those modules don't get along.
3) I'm not too sure, if they would accept hacks like this one.

@Patrick... Couple of question about the mod you have on your blog post:

1) Am I correct in assuming that files uploaded using the CCK filefield are only available to individuals with access to the node it is attached to and doesn't deal with permissions at all?

2) Any idea if this would conflict with the following modules?
imagefield
filefield_paths
file_aliases

3) Are you planning to submit this to the drupal.org site?

Hi,
my problem is, that I use organic groups, so adding an extra permission is not an option for me. What I need is file protection based on node access control. I fiddled around with your solution and came up with a wrapper around CCK File Field:
http://www.onyxbits.de/content/drupal-and-problem-protecting-uploaded-files

Worked it out. You leave the files in the upload folder where they were, and add the .htaccess file

Since this path now doesn't physically exist on your server (it shouldn't), the request will be passed to Drupal. Let's respond to it by implementing hook_menu.
If the path isn't on the server where do I put the actual files?
thanks!

I think I have got the wrong end of the stick here. I've set up two folders on my website, used the private downloads module but when I link to the file in the website it refers to the file but is unprotected.
Where is the system folder supposed to be located? I have it in my public_html folder, is that correct? Thanks

Hi folks - I'm not quite understanding how this code works well enough to modify it to allow different permissions for different directories. Can any one point me in the right direction? Thanks.

I have just released a full-fledgedPrivate Download module based on this article. Props go to the author for originating the idea.

Works GREAT, thanks !!!

Ups, I didn't notice that comments are in reverse order! Could you remove the duplicate?

If you are interested, here's a new manual page.
http://drupal.org/node/540754
"Restrict specific folders from public download (via .htaccess)"

If you are interested, here's a new manual page.
http://drupal.org/node/540754
"Restrict specific folders from public download (via .htaccess)"

@DrupalSav: Thank you.

I had a problem with the names, just not where you suggested. I changed the names to the folder I was using. I just didn't change the part with 'page arguments' => array('privatedownloads'), because I thought this was an some kind of argument Drupal needed. It didn't occur to me that this also was the name of the folder. I changed it and now it works just fine. My mistake.

Perhaps you've already solved this, but just in case .... I got this affect when the folder name in my .htaccess file did not match the folder name in hook_menu. Where it says "$items['system/files/privatedownloads']" that must be the same as in .htaccess "RewriteBase /system/files/privatedownloads" When I had them different it gave me 404's.

This method looks great so far but I do have a few questions...

1) Does this method only allow for manually FTPing files to this private directory or can we use CCK filefield to upload private files?

2) Does this conflict with any other modules that we know of?

Hi,
seems this works for some people. But if I protect a file, guests can't access the files, and I can't either. I keep getting sent to my 404-page. Any ideas why that might be the case? I already tried module-weight, which doesn't seem to do the trick.

Hi ,
M new in drupal , It great work ,
my problem is that i m using public counter module (http://drupal.org/project/pubdlcnt) to count the numbet of downloads, It not working there ,
I kept the file system as 'public' , and want to keep as it is.
I have also use the download count module that work for private download , but it's not working
can u tell me any settings in public counter module to be work as a counter

Thnx in advnc

It took some digging, but I found a solution to the aforementioned IE problem.

Replace the previously defined hdpug_file_download() function with the following:


function hdpug_file_download($file) {
return array('Content-Type: '. file_get_mimetype($file), 'Content-Transfer-Encoding: binary', 'Cache-Control: max-age=60, must-revalidate');
}

Note the additional attributes to force IE to behave.

Also note the first line was removed because it didn't provide any purpose.

I'm not sure how this problem got past the author, but in the end the overall solution works quite well with this fix in place.

Unfortunately this produces a nasty error in IE when the server is configured with the non-cache option. I believe this can be overcome with some additional mod_header related lines in .htaccess, but so far my attempts have failed. If anyone has any suggestions or ideas please share.

Thanks for posting this extremely useful tip! It's perfect for adding private downloads without needing node file attachments.

To clarify for those confused about how to implement this, here's a quick recap:

1) Create a module file (exp. mymodule.module) with the following contents:

<?php

/**
* Implementation of hook_perm().
*/
function mymodule_perm() {
return array('access private downloads folder');
}

/**
* Implementation of hook_menu().
*/
function mymodule_menu() {
$items = array();

$items['system/files/privatedownloads'] = array(
'access arguments' => array('access private downloads folder'),
'page callback' => 'file_download',
'page arguments' => array('privatedownloads'),
'type' => MENU_CALLBACK,
);

return $items;
}

/**
* Implementation of hook_file_download().
*/
function mymodule_file_download($file) {
$info = image_get_info(file_create_path($file));
return array('Content-type: '. file_get_mimetype($file));
}

2) Create an info module (exp. mymodule.info) with the following contents:


name = mymodule
description = Adds private file download support.
core = 6.x

3) Create the privatedownloads folder in /files.
4) Add the aforementioned .htaccess file to /privatedownloads.
5) Add some private files to the folder and enabled the module.

Post links for your private files like so:

www.domain.com/files/privatedownloads/test.jpg

Works like a charm!

do anyone make this working for IMCE? thanks

Hi Davy,

This made the solution I am after a lot more elegant, straightforward and most important - secure.
In addition I get now back the option to use css and js aggregation functions (performance page).
Excellent!

2 comments/questions:
1. Not sure why did you implemented the hook_menu - I did not implement it and still file_download hook is being called whenever 'system/files/' is being accessed. Did I miss something?
2. Placing the get_mime call at the end of hook did not change anything - for some reason pdfs on my site won't open up in the browser but rather trigger the download pop-up. why is that?

Thanks!

You have to implement hook_perm


function your_module_perm() {
return array('your permission');
}

So, I did some learning and figured out how to create a module and get it installed. Everything seems fine, except that there is no access private downloads folder permission showing on the permissions page anywhere. Does this happen for anyone else?

Like Christian, I'm not sure where to put the functions you laid out. I'm just getting into Drupal and have never made a module or anything. I really would appreciate this mix of public and private downloads for use with IMCE. I don't want to have to attach files to nodes as private_upload requires.

Another step or two describing where to put those functions would be fantastic. Thanks!

This was exactly my dilemma also, this is a clever solution. I don't know where to put the calls to the Drupal though? Am I basically making a new module? Can I put both of those calls into a new .module file? Anything to point me in the right direction would be appreciated.

I've seen this module too. But it only works for the Upload module. I'm using IMCE.

Thanks for the tips! I will be implementing this, but I have one small problem - it doesn't work when Drupal is in a sub-directory. Eg:

http://localhost/mydrupal/sites/all/files/privatedownloads/pic.png

gets rewritten as

http://localhost/system/files/privatedownloads/pic.png

Notice that the base directory is lost. I realise I could hard-code this value into my .htaccess, but ideally I'd like it to still work when I checkout the site on my live server (where Drupal IS in the root directory).

Any ideas ? Thanks again!

hi,

Private Upload module does some of this.

i've migrated sites from private-only to a mix using this module, so it might be of use to others in a similar boat.

justin

no, but when i change the scheme, i can do a simple url rewriting. i think the easy solution in your case is .htaccess rewriting based on the ids.

I notice that http://www.drupalcoder.com/story/406-mixing-private-and-public-downloads... is the same as http://www.drupalcoder.com/node/406.

I have been looking for a means by which if the url generated by pathauto changes (eg when the titleis changed), the url is prefixed with a number which allows the article to be located by the node id.

Is this what your pathauto configuration accomplishes?

I am looking for something like that

Post new comment

The content of this field is kept private and will not be shown publicly.
  • Web page addresses and e-mail addresses turn into links automatically.
  • Allowed HTML tags: <a> <em> <strong> <cite> <code> <ul> <ol> <li> <dl> <dt> <dd>
  • Lines and paragraphs break automatically.

More information about formatting options