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));
}
Comments
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.
thanks!
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 folderpermission 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
Hi Davy,
Thx for your posting. I have used this kind of configuration too (http://drupal.org/node/189239), but had problem with the mimetype of pdf ... you show me the path to resolve this issue !
Happy drupal ;-)
Simple, straightforward, effective. Useful post, thanks!
Post new comment