General developer forum

Creating and downloading a file

 
 
Picture of Dave Emsley
Creating and downloading a file
 

I'm trying to create a CSV file that can be downloaded by the user.

I've tried to follow the API documents and get most of it working.  Here's my code:

 

$context = context_course::instance($COURSE->id);
$fs = get_file_storage();    // Prepare file record object    
$fileinfo = array(   
'contextid' => $context->id, // ID of context
    'component' => 'block_voucher',     // usually = table name   
'filearea' => 'voucher',     // usually = table name
    'itemid' => $timestamp,      // usually = ID of row in table
    'filepath' => '/',           // any path beginning and ending in /
    'filename' => $filename); // any filename     
// Create file containing text     
$fs->create_file_from_string($fileinfo, $stringtowrite);  //String to write is the CSV contents

$url = moodle_url::make_pluginfile_url($fileinfo['contextid'], $fileinfo['component'], $fileinfo['filearea'], $fileinfo['itemid'], $fileinfo['filepath'], $fileinfo['filename']);

echo "<a href='".$url."'>Click to download</a>";

Clicking the link gives errors, I tried using... http://docs.moodle.org/dev/File_API#Serving_files_to_users

but I get the errors:

 

Debug info:
Error code: filenotfound
Stack trace:
  • line 463 of /lib/setuplib.php: moodle_exception thrown
  • line 1948 of /lib/filelib.php: call to print_error()
  • line 4610 of /lib/filelib.php: call to send_file_not_found()
  • line 37 of /pluginfile.php: call to file_pluginfile()

I cannot find the file as a "real" file anywhere on the server although I am assuming it is stored in the database.

 

Cheers

 

Dave

 

 
Average of ratings: -
Davo
Re: Creating and downloading a file
Group DevelopersGroup Particularly helpful Moodlers

Have you added a function to do any security checks and then serve your file?

blocks/voucher/lib.php:

function block_voucher_pluginfile($course, $cm, $context, $filearea, $args, $forcedownload, array $options=array()) {
// Do various security checks and then call send_stored_file()
}

See mod/forum/lib.php, forum_pluginfile(...) for an example.

 
Average of ratings:Useful (1)
Picture of Dave Emsley
Re: Creating and downloading a file
 

Hi Davo,

I had included this in the the block's lip.php file but obviously I'm doing something not quite right.

 

Thanks

Dave

 
Average of ratings: -
Davo
Re: Creating and downloading a file
Group DevelopersGroup Particularly helpful Moodlers

At this point, I usually find the easiest solution is to make sure you've got xdebug + a suitable IDE configured, put a breakpoint in pluginfile.php and then step through to see where it's going wrong.

If that's not possible, then the next best thing is to add some echo statements into pluginfile.php, your block_voucher_pluginfile function and the function calls in between the two (which you can find by reading the code), to see what values are where and at which point the code is bailing out.

 
Average of ratings:Useful (1)
Picture of Dave Emsley
Re: Creating and downloading a file
 

I've whacked in the echo statements as you suggested and it seems my block_voucher_pluginfile is not being called - at least the echos of variable content and print_r for arrays and print_objects are not showing up.

The URL generated by: $url = moodle_url::make_pluginfile_url($fileinfo['contextid'], $fileinfo['component'], $fileinfo['filearea'], $fileinfo['itemid'], $fileinfo['filepath'], $fileinfo['filename']);

 

is: http://192.168.1.40/~magicalphotocourse/pluginfile.php/2/block_voucher/voucher/0/1396008086.csv and this looks right from the data in the mdl_files table.

I'm under the impression that the function should get called automatically? Or do I need to call it from somewhere?

 

Just a quick thought is the the ~ symbol a problem?

 

Cheers


Dave

 
Average of ratings: -
Davo
Re: Creating and downloading a file
Group DevelopersGroup Particularly helpful Moodlers

The '~' has been known to cause problems with some setups and some browsers (but I think that it's been fixed in more recent Moodle versions).

I suggest you need to add some extra echo / print_r statements to pluginfile.php and the file_pluginfile function this calls (in lib/filelib.php). From the URL, I'd be very suspicious of contextid = 2 - that really doesn't look high enough to be a block context (and the code in filelib.php does some checks based on the context).

 

 
Average of ratings:Useful (1)
Picture of Dave Emsley
Re: Creating and downloading a file
 

function block_voucher_pluginfile....

   if ($context->contextlevel != CONTEXT_MODULE) {
return false;
}

This appears to be where I'm going wrong as 30 != 70 but that's meaningless to me. I obviously don't understand the concept of contexts.

BTW @Davo  $context = context_user::instance($USER->id); is now returning 5 as the $context->id - is that high enough?

Cheers


Dave

 

 
Average of ratings: -
Davo
Re: Creating and downloading a file
Group DevelopersGroup Particularly helpful Moodlers

Contexts are the different places within the system where people can be assigned roles, files can be stored or comments added (amongst other uses).

There is a hierarchy of contexts, starting with CONTEXT_SYSTEM, which represents roles / files that are allocated / stored at the whole site level.

Within CONTEXT_SYSTEM there are multiple CONTEXT_COURSECAT, which represents a course category (you get a category instance by calling context_coursecat::instance($categoryid)).

Within that are multiple CONTEXT_COURSE instances, one for each course (context_course::instance($courseid)). Then you find CONTEXT_MODULE (activities / resources - context_module::instance($cmid)) and CONTEXT_BLOCK (for blocks).

(Separately there is also CONTEXT_USER - for the user profile page).

If your files are to be stored within your block, then you need to get the context for your particular block, usually via $context = context_block::instance($blockinstanceid); From within the block, you can get the instanceid by calling $this->instance->id, it is the 'id' field from the table mdl_block_instances. Actually, from within the block, you can shortcut all of this by simply calling $this->context, as it's already been worked out for you.

So, after all that, the check you should be making, if you've used the block context, is that the $context->contextlevel == CONTEXT_BLOCK.

 
Average of ratings:Useful (2)
Picture of Gerry Hall
Re: Creating and downloading a file
 

Hi Dave,

I think the problem is that you are setting the file as CONTEXT_COURSE  but trying to get the file as CONTEXT_MODULE

$context = context_course::instance($COURSE->id);
$fs = get_file_storage();    // Prepare file record object    
$fileinfo = array(   
'contextid' => $context->id, // ID of context
    'component' => 'block_voucher',     // usually = table name   
'filearea' => 'voucher',     // usually = table name
    'itemid' => $timestamp,      // usually = ID of row in table
    'filepath' => '/',           // any path beginning and ending in /
    'filename' => $filename); // any filename     
 if ($context->contextlevel != CONTEXT_MODULE) {
     return false;
    }

you need to make sure the values in the create $fileinfo array are exactly the same to the params in the pluginfile method in your lib.php

is if you are saving the file at in course context then

if ($context->contextlevel != CONTEXT_COURSE) {
     return false;
    }

the above is just an example you need to make sure that you save the file in the right context to ensure your capabilities work as expected

 

 
Average of ratings:Useful (1)
Rosario playing soccer
Re: Creating and downloading a file
 

Dear all, I need a help from you too. I wrote a Block with a button or simple link to zip and download all published files of the course where the block is shown. This is useful both for teachers and students to download all files at once. Strangely there is a DOWNLOAD ALL function only for the teachers and only if the files are placed in a folder.

  • I create a list of published files with an SQL-query, works fine, I get the list
  • I get the needed data using list($context, $course, $cm) = get_context_info_array... ,works great
  • I get the fileinfo by looping through the file list using get_file_browser and $browser->get_file_info with the above data, works like a charm
  • I get the single file from the storage with $f = $fs->get_file, another reason to praise all involved core developers, works fine too
  • with $cm I got from above, I can check if the file is visible. If the user is a teacher include it in the zip-file, otherwise skip it, as students are not allowed to download hidden sections/files
  • as per File Api, you have to copy a file to an OS-path with $f->copy_content_to($CFG->dataroot . '/temp/zip/' . $file->filename, works fine
  • I got a new zip archive with $zarc = new zip_archive(); and $zarc->open($CFG->dataroot . '/temp/zip/sess_' . sesskey() . '_' . $course->shortname . '_files.zip', I use the users' session key to not mess up the zip-file-names in concurrent production environment. I would rather prefer putting everything into /temp/zip/sessionkey/ but the zip_archive::open function ignores all paths different from temp/zip, works fine, no problem for me
  • I can add all files in the loop to the zip_archive with $zarc->add_file_from_pathname, works fine
  • I can delete all copied and added files from the temp/zip directory with unlink($CFG->dataroot . '/temp/zip/' . $file->filename, works fine
  • I can delete the zip_archive file in temp/zip with unlink too
  • THE ONLY THING I CAN NOT DO is serve this file to the user for download, as I have no clue on how to do it

In Moodle 1.9 I simply composed the URL like yourmoodle.com/temp/zip/myZipArchive.zip and that worked.

Now such an URL yields an error. So I tried with redirect(new moodle_url... using different paths and using also

redirect(new moodle_url('/pluginfile.php/' . $context->id . '/content/temp/zip/sess_' . sesskey() . '_' . $course->shortname . '_files.zip', array('id'=>$courseid)));

I studied also the code for the own callback function needed to use pluginfile.php but I did not implement it yet. Before doing so, I wanted to ask you for advice. It took me 3 days to implement most of the code. I hope it won't take me 3 weeks and more to implement serving that file to the users' browser.

Here is the thread where I presented my project: https://moodle.org/mod/forum/discuss.php?d=218220

Regards and many thanks, Rosario



 
Average of ratings: -
Davo
Re: Creating and downloading a file
Group DevelopersGroup Particularly helpful Moodlers

Take a look at how mod_assign does this in mod/assign/locallib.php, function download_submissions()

You'll see it generates an array of stored_file objects, then calls zip_packer->archive_to_pathname to zip these into a temporary file, before calling send_temp_file to send it to the user's browser (and delete the zip file).


 
Average of ratings: -
Rosario playing soccer
Re: Creating and downloading a file
 

Davo, as always, thanks a lot for your hints. I will have a look at it and report back.

Rosario

 
Average of ratings: -
Rosario playing soccer
Re: Creating and downloading a file
 

Davo, I programmed until 24 o' clock... but I am almost through. I still got everything, including the new zip file created with zip_packer instead of zip_archive (who knows what the difference is?)

One last problem, I get the following error as soon as I call send_temp_file($path, $filename, $pathisstring=false)

Warning: Cannot modify header information - headers already sent by (output started at /.../htdocs/moodleTest23/htdocs/blocks/download_all_published_files/download_all_published_files.php:174) in /.../htdocs/moodleTest23/htdocs/lib/filelib.php on line 2129 [long repetition and finally] /.../htdocs/moodleTest23/data/temp/zip/testShortName_files_bsNTWM

If I call with $pathstring = true, I get a cluttered output directly as html-page, but not as download-file.

The above temporary zip-file testShortName_files_bsNTWM is correctly created, has all files neatly zipped in it, I can download it via ssh and verify its content. Could it be we can not use send_temp_file out of my block-code? If I would have to use pluginfile.php I rather guess I would have sort of check in the zip file into a temporary file area of the block, or would have to use functions to create the zip file directly in the block's filearea.

Maybe I have to clean up my code first, as I am calling, from my block_download_all_published_files.php code I put into another file I named download_all_published_files.php and this might break the Moodle /block/download_all_published_files URL behaviour and could also explain why my first attempt to do it with pluginfile.php and its callback funktion did not work.

Any idea? Thanks, Rosario

 
Average of ratings: -
Davo
Re: Creating and downloading a file
Group DevelopersGroup Particularly helpful Moodlers

Make sure the page on which you call 'send_temp_file' does not output anything else at all.

e.g. you should have a link to click saying 'download all files', and the PHP script this links to should gather all the files and then call 'send_temp_file'; it should not do any output other than that.

 
Average of ratings: -
Rosario playing soccer
Re: Creating and downloading a file
 

Oh, thanks, you might be very very right, as I do a lot of echo and print_r to debug and follow the code.

I will report back, Rosario

 
Average of ratings: -
Picture of David Mudrák
Re: Creating and downloading a file
Group DevelopersGroup Documentation writersGroup Moodle HQGroup Particularly helpful MoodlersGroup Translators

You can also find this wrapper useful (I use it a lot for debugging, too)

print_object($something);
 
Average of ratings: -
Rosario playing soccer
Re: Creating and downloading a file
 

David, thanks. I took the chance to clean up my code a little bit. 

I am actually using

add_to_log($courseid, 'download_files', 'addtoarchive', '', get_string('zipfailed', 'block_download_all_published_files'));

print_error('zipfailed', 'block_download_all_published_files', new moodle_url('/course/view.php',array('id'=>$courseid)));

And I was using print_r for debugging in my first Moodle programs and var_dump. I still have not the whole overview, I mean I am not able to make use of those functions blindly and easily, I still have to learn this lesson.

I saw also

debugging("Can not zip '$archivepath' directory", DEBUG_DEVELOPER);

in the zip_packer code, I think. So I still have to learn in which situations to use which of those functions.

I have to decide when a silent debug message is better than a print_error which informs the user that something went wrong. Adding to the log is very clear to me.

And print_object would output without interfering with the headers/requests/query ?

Rosario

 
Average of ratings: -
Picture of David Mudrák
Re: Creating and downloading a file
Group DevelopersGroup Documentation writersGroup Moodle HQGroup Particularly helpful MoodlersGroup Translators

No. The print_object() is just an equivalent for the print_r() actually. debugging() should be useful (especially combined with DEBUG_DEVELOPER) if you want to keep the message being displayed at certain place for other developers as well. We use it when a deprecated function is called, for example.

add_to_log() or its recent variant should be used if the action is something worth of keeping in the Moodle logs itself. Not a good thing for debugging to be honest.

 
Average of ratings: -
Rosario playing soccer
Re: Creating and downloading a file
 

Yes, you are right, I will reduce my add_to_log calls to a strict minimum. In a hurry I simply converted print_r and echo to add_to_log, so that every single file processed/added to archive/etc. would create a new log entry, which definitly is too much. I think one call upon entry und maybe one on exit is enough for the Moodle logs. And the rest can be done with print_error and debug calls.

Rosario

 
Average of ratings: -
Rosario playing soccer
Re: Creating and downloading a file
 

YES! this works now! after removing all my debug-outputs.

Only a minor problem, user experience: in my FireFox I set up the download directory to be c:\temp, so send_temp_file works perfectly fine, but the user is not aware that the download happened in the background. Of course I could now output "the zip archive has been downloaded to your download directory! Please check there." or something the like.

Another minor problem but to be solved in my second version I think is this:

for the moment being the zip file contains all the published files visible to the students but only in a flat hierarchy. In the next version it would be nice to reflect the courses' own hierarchy like this:

Topic1/firstFile.pdf

Topic1/secondFile.pdf

Topic2/rootFolder/thirdFile.pdf

Topic2/rootFolder/firstNestedFolder/fourthFile.pdf

and so on. I am only feeling, I would have to find all the modules and then loop through them and in case of folders, it would become sort of recursive iteration through the nested folders and the contained files in there.

If this is not worth the work, I could also simply try to reflect only the Topics in which the files and folders containing files reside. The result would be a flatter hierarchy like this:

Topic1/firstFile.pdf

Topic1/secondFile.pdf

Topic2/thirdFile.pdf

Topic2/fourthFile.pdf

If you need the sql-query which yields me the list of all files contained in a course, let me know. There might be a simple extension to it to get all needed path-information.

Rosario (very very happy with my code I will upload as block in the next days after cleanup)

Davo, it is again YOUR CREDIT, thanks a lot.


 
Average of ratings: -
Rosario playing soccer
Re: Creating and downloading a file
 

Here you can find my first beta of the block download_all_published_files:

https://moodle.org/mod/forum/discuss.php?d=218220

Rosario (any hints on how to redirect after send_temp_file and how to get the folder-hierarchy and the Topic names in which the files reside are stil appreciated.)

 
Average of ratings: -
Rosario playing soccer
Re: Creating and downloading a file
 
Davo, I found a hint in the course/view.php on how to list the sections/topics and correlate to the files contained in them:

$modinfo = get_fast_modinfo($course);
print_object($modinfo);

This yields me a long list and I simply show here the data that is interesting for me:

    [cms] => Array
        (
            [4] => cm_info Object
                (
                    [modinfo:cm_info:private] => course_modinfo Object
 *RECURSION*
:
:
                    [modname] => resource
                    [module] => 17
                    [name] => studIntroD.pdf
                    [sectionnum] => 3
                    [section] => 28

So, we can see the 'name' field containing the filename and 'section' containing the section/topic in which it resides. The section 28 is also to be found in the same output as:

            [3] => section_info Object
                (
                    [id] => 28
                    [course] => 4
                    [section] => 3
                    [name] => Topic 3
                    [visible] => 1
                    [summary] =>
                    [summaryformat] => 1
                    [showavailability] => 0
                    [availablefrom] => 0
                    [availableuntil] => 0
                    [groupingid] => 0
                    [conditionscompletion] => Array

I could take the 'name' from the section_info Object, "Topic 3" and combine it with the course_modinfo Object 'name' to compose the filename Topic 3/studIntroD.pdf

That's ok, but I need a beginners help on how to access those deeply nested objects. Looping through

$modinfo['sectioninfo:course_modinfo:private'] should give me the section/topics names and their ids

Then looping through

$modinfo['cms']  should give me the filenames and the section ids.

Then let me try it, unless you see a simpler sql-query or approach to get this.

Rosario





 
Average of ratings: -
Rosario playing soccer
Re: Creating and downloading a file
 

Thanks again, I figured out an algorithm to loop through the needed data structures. See in the above thread for details, where I uploaded also my second beta.

Rosario

 
Average of ratings: -