Using The Moodle File API in Modules

Moodle uses a virtual file system on top of the 'physical' directory structure located in moodledata. Each file is stored only once in a physical location, which is referenced in the database. The file is identified using a unique hash (based, of course, on the content of the file).

So, whenever that file is copied, or renamed, the actual file remains the same. Only the references in the database are changed and stored in combination with the hash. This also means that uploading the same file twice does not take up any more space on the hard drive. Even if you rename the file before the second upload, Moodle will still maintain only one copy of the file.

Why Use Moodle's File API At All?

Why would you want to use the Moodle file API in your modules? After all, simply saving an uploaded file to the server is not that hard. Well, apart from the reason that Moodle always only keeps one copy (saving hard disk space), it also makes it much easier to program backup and restore functions for your module.

To use Moodle's file API in your own modules, you need to follow a few conventions. Before we get to that, let's discuss the database table mdl_files first.

Where Are My Files Actually Stored?

As stated before, the actual files are still stored on the server's hard drive, usually inside the moodledata/filedir directory. A reference to the file is kept in mdl_files.contenthash. The first two pairs of characters of the content hash are used to create two subdirectories. For instance, if your file's content hash is f3d58f82c21e90df4e00cc1c2c081c148c6ac686, the file will be stored in:

moodledata/filedir/f3/d5

You can see this for yourself by uploading a file, for instance through the assign module, and then checking the content of the most recently added mdl_files record (provided you never uploaded the file before). You should be able to find the content hash and match it with the directory structure and finally the name of the physical file.

The moodle.org website contains some documentation on how to store an arbitrary data blob or string: http://docs.moodle.org/dev/File_API#Create_file.

Meaning of The Columns in The 'mdl_files' Table

Here's a brief primer on the anatomy of the mdl_files table:

  • contenthash: unique hash of the file. Moodles stores incoming files under this name.
  • pathnamehash: hash of the 'virtual' filepath and filename
  • contextid: id of an mdl_context record
  • component: name of the component (e.g. a plugin) which created or uploaded the file
  • filearea: arbitrary string to further subdivide virtual files into groups
  • itemid: id of table (optional) containing information pertaining to the file
  • filepath: virtual path
  • filename: original filename (remember: the file itself is renamed to the contenthash by Moodle after uploading)
  • userid: uploader or creator
  • filesize: 0 seems to be used to indicate a “directory”. For some reason, Moodle creates a record with filesize 0 for each new file, in addition to the 'valid' record.

Conventions For Handling Files in Your Module

Modules are supposed to stick to a few rules concerning files:

  • Save files using mod_[your-mod-name] as the value for the component column
  • Create a callback function [your-mod-name]_pluginfile in your mod's lib.php file

We will explain the two rules in the following sections.

Saving Files

To save an uploaded file, you should first check whether it already exists as the exact same virtual file that you intend to create. Let's look at some code to see what that means.

First of all, get the uploaded file:

$uploaded_file_path = $_FILES['user_file']['tmp_name'];  // temp path to the actual file
$filename = $_FILES['user_file']['name'];                // the original (human readable) filename

Then, create an instance of Moodle's file_storage class:

$file_storage = get_file_storage();

We are going to use the $file_storage object to:

  1. Check if the file already exists, and if not:
  2. Actually create the file inside Moodle's virtual file system

So, here we go:

$context = context_module::instance($id);
$fileinfo = array(
    'contextid' => $context->id,
    'component' => 'mod_rainmaker',       // mod_[your-mod-name]
    'filearea' => 'rainmaker_docs',  // arbitrary string
    'itemid' => $id,               // use a unique id in the context of the filearea and you should be safe
    'filepath' => '/',             // virtual path
    'filename' => $filename);      // virtual filename

$file_storage = get_file_storage();
if ($file_storage->file_exists($fileinfo['contextid'],
                     $fileinfo['component'],
                     $fileinfo['filearea'],
                     $fileinfo['itemid'],
                     $fileinfo['filepath'],
                     $fileinfo['filename'])) return false; // (this code is actually in a function)

$file = $file_storage->create_file_from_pathname($fileinfo, $uploaded_file_path);

As you can see, the exact same array of file information that is used to store the new file, is also used to check for the presence of an existing file. Remember, this is not a check for the physical file, but for the 'virtual' file with all the same properties.

If you omit this check, and you upload the exact same file twice, Moodle will give an error, because it would result in a duplicate mdl_file.pathnamehash value.

Serving Files

To serve files from inside you module, you need to:

  • Create a link to a specific file
  • Add a '[your-mod-name]_pluginfile' function to your module's lib.php file

Create A File Link

As stated in See http://docs.moodle.org/dev/File_API#Serving_files_to_users, call moodle_url::make_pluginfile_url.

The function moodle_url::make_pluginfile_url takes a lot of specific arguments, so we have split this task into two parts:

  1. get file information
  2. using that information, create the url

Here's the code for retrieving the file information:

function get_rainmaker_file($course_module_id) {
    $context = context_module::instance($course_module_id);
    $fs = get_file_storage();
    $files = $fs->get_area_files($context->id, 'mod_rainmaker', 'rainmaker_docs', $course_module_id, $sort = false, $includedirs = false);
    if (!count($files)) return false;
    return array_shift($files);
} // function get_rainmaker_file

Please be sure to change mod_rainmaker to your own module's name.

Using the file information, we can now construct the url.

function get_rainmaker_doc_url() {
    global $id; // the course_module id
    if (! $file = get_rainmaker_file($id)) return false;
    return moodle_url::make_pluginfile_url(
        $file->get_contextid(),
        $file->get_component(),
        $file->get_filearea(),
        $file->get_itemid(),
        $file->get_filepath(),
        $file->get_filename(),
        $forcedownload = true);
} // function get_rainmaker_doc_url

Calling the get_rainmaker_doc_url function will return a url which you can display in your module's “views”.

Serving The File

If users click the newly constructed url, they'll be redirected to something like:

/pluginfile.php/100/mod_rainmaker/rainmaker_docs/64/test.docx?forcedownload=1

Unfortunately, that's not enough. The pluginfile.php script is going to look for a callback function, which should be located in your module's lib.php.

Here's an example:

function rainmaker_pluginfile($course, $cm, $context, $filearea, $args, $forcedownload) {
    global $CFG;

    require_login($course, true, $cm);

    if (! $file = get_rainmaker_file($cm->id)) return false;
    send_stored_file($file);
} // function rainmaker_pluginfile

Or a more detailed example:

function local_section_icons_images_pluginfile($course, $cm, $context, $filearea, $args, $forcedownload, array $options=array()) {

    // Check the contextlevel is as expected - if your plugin is a block, this becomes CONTEXT_BLOCK, etc.
    if ($context->contextlevel != CONTEXT_COURSE) {
        return false; 
    }

    // Make sure the filearea is one of those used by the plugin.
    if ($filearea !== 'section_icons_images') {
        return false;
    }
 
    // Make sure the user is logged in and has access to the module (plugins that are not course modules should leave out the 'cm' part).
    require_login($course, true, $cm);
 
    // No check for capability, because everybody needs to see it
    // Check the relevant capabilities - these may vary depending on the filearea being accessed.
    /*
	if (!has_capability('mod/bookcase:addinstance', $context)) {
        return false;
    }
    */
 
    // Leave this line out if you set the itemid to null in make_pluginfile_url (set $itemid to 0 instead).
    $itemid = array_shift($args); // The first item in the $args array.
 
    // Use the itemid to retrieve any relevant data records and perform any security checks to see if the
    // user really does have access to the file in question.
 
    // Extract the filename / filepath from the $args array.
    $filename = array_pop($args); // The last item in the $args array.
    if (!$args) {
        $filepath = '/'; // $args is empty => the path is '/'
    } else {
        $filepath = '/'.implode('/', $args).'/'; // $args contains elements of the filepath
    }

    // Retrieve the file from the Files API.
    $fs = get_file_storage();
    $file = $fs->get_file($context->id, 'local_section_icons_images', $filearea, $itemid, $filepath, $filename);
	if (!$file) {
        return false; // The file does not exist.
    }
 
    // We can now send the file back to the browser - in this case with a cache lifetime of 1 day and no filtering. 
    // From Moodle 2.3, use send_stored_file instead.
    send_stored_file($file, 86400, 0, $forcedownload, $options);
}

Attachments in Mod Instance Forms (mod_form.php)

See mod assign for an example of how to add a filemanager for attachments.

In your mod_form.php:

function definition() {
    // other form elements
    $mform->addElement('filemanager', 'attachments', get_string('contentimage', 'mod_rainmaker'), null, $filemanager_options);
    // remaining form elements
}

function data_preprocessing(&$default_values) {
    if ($this->current->instance) {
        // ...
       
        //files
        $draftitemid = file_get_submitted_draft_itemid('attachments');
        file_prepare_draft_area($draftitemid, $this->context->id, 'mod_rainmaker', 'attachments', 0, array('subdirs'=>true));
        $default_values['attachments'] = $draftitemid;
        
        // ...
    }
}

In your mod's lib.php file:

function rainmaker_save_draft_area_files($data) {
    $context = context_module::instance($data->coursemodule);
    if (isset($data->attachments)) {
        file_save_draft_area_files($data->attachments, $context->id,
                                   'mod_rainmaker', 'attachments', 0, array('subdirs' => 0, 'maxbytes' => $maxbytes, 'maxfiles' => 50));
    }
} // function rainmaker_save_draft_area_files

function rainmaker_add_instance(stdClass $data, mod_rainmaker_mod_form $mform = null) {
    global $DB;
    
    fasetwoelement_save_draft_area_files($data);
    // ... remaining code
}

function rainmaker_update_instance(stdClass $data, mod_rainmaker_mod_form $mform = null) {
    global $DB;

    fasetwoelement_save_draft_area_files($data);
    // ... remaining code
}

Personal Tools